最近看了一下SpringSecurity方面的东西,之前都是接手项目,权限什么的都做好了,现在有点时间正好看一下,顺带做个最简单的Demo,进行一个初步的学习。
1、环境搭建(gradle版本是6.8)
plugins {
id 'org.springframework.boot' version '2.6.6'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group 'org.example'
version '1.0-SNAPSHOT'
repositories {
maven { url "https://maven.aliyun.com/nexus/content/groups/public"}
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.baomidou:mybatis-plus-boot-starter:3.2.0'
runtimeOnly 'mysql:mysql-connector-java:8.0.25'
testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:2.2.0'
}
2、项目结构:
3、核心配置
3.1、UserService:主要实现SpringSecurity的UserDetailsService
package wyf.application.service.impl;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import wyf.domain.SecurityUserDetails;
import wyf.domain.User;
import wyf.repository.UserRepository;
import wyf.application.service.UserService;
import javax.annotation.Resource;
import java.util.Objects;
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findUserByName(username);
if (Objects.nonNull(user)) {
return new SecurityUserDetails(user);
}
return null;
}
}
loadUserByUsername方法的返回值是UserDetails,所以需要一个类去实现SpringSecurity提供的UserDetails接口
3.2、SecurityUserDetails:实现UserDetails接口
package wyf.domain;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
public class SecurityUserDetails implements UserDetails {
private User user;
public SecurityUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return AuthorityUtils.createAuthorityList(user.getRoles());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
3.3、User类:用户类,关键是用户角色设置有要求,必须以"ROLE_"开头
package wyf.domain;
import java.util.List;
import java.util.Objects;
public class User {
private long id;
private String username;
private String password;
private List<Role> roleList;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public List<Role> getRoleList() {
return roleList;
}
public void setRoleList(List<Role> roleList) {
this.roleList = roleList;
}
public String getRoles(){
StringBuilder sb = new StringBuilder();
if (Objects.nonNull(roleList)) {
for (Role role : roleList) {
sb.append("ROLE_").append(role.getName()).append(",");
}
}
return sb.substring(0,sb.length() > 0 ? sb.length() - 1 : sb.length());
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + username + ''' +
", password='" + password + ''' +
", roleList=" + roleList +
'}';
}
}
至于为什么要以ROLE_开头,这是SpringSecurity的要求,否则检查用户角色时会报错,下面的配置类中有方法hasAnyRole方法或者用hasRole方法也许,源码里有这样一段代码检查用户拥有的角色,按它的检查原则,我们自己存的角色名要么在数据库里存成以ROLE开头的,要么自己组装一下放在内存,我选择后者。源码如下:
这个rolePrefix就是ROLE_在源码中如下:
我是这么理解的,因为我不加ROLE_前缀会报错403,然后点源码看到的,上面那些代码干了啥,我还没去看,之后再看,先把它用起来。
3.4、SecurityConfig:SpringSecurity配置类
package wyf.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import wyf.application.service.UserService;
import javax.annotation.Resource;
@Configuration
@EnableWebSecurity//用于启用Web安全的注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserService userService;
// 进行认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
// 配置拦截
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/user/getUser")
.hasAnyRole("admin")
.antMatchers("/user/login")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
以上是SpringSecurity的核心配置类,UserService是一个继承了SpringSecurity中UserDetailsService接口的接口,它的实现类主要是从数据库查询出号密码以及相关角色、权限等进行比对。PasswordEncoder是SpringSecurity对密码的加密解密bean。配置拦截主要是说"/user/login"路径允许访问,"/user/getUser"需要admin角色才能访问,formLogin表示使用SpringSecurity默认的登录页面。
4、数据库配置:
4.1、t_user表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
`phone_number` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号',
`enable` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '是否可用:0-可用,1-不可用',
`create_time` datetime NOT NULL COMMENT '创建时间',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '创建人',
`update_time` datetime NOT NULL COMMENT '更新时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '更新人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_user
-- ----------------------------
INSERT INTO `t_user` VALUES (1, '张三', '$2a$10$x0OunSegxpe5/9/YS0XNauvrHWFnnT0kwkv/Xb9e3FrH0Lmz6rx/e', '15235084160', '0', '2022-04-05 22:31:03', 'admin', '2022-04-05 22:31:11', 'admin');
INSERT INTO `t_user` VALUES (2, '李四', '$2a$10$hYG4KPj6ypeY.QMfndlEqeXMN1YM86dskMXBrKluxKtHZz3Y1.U5K', '11111111111', '0', '2022-04-07 14:28:08', 'admin', '2022-04-07 14:28:14', 'admin');
SET FOREIGN_KEY_CHECKS = 1;
4.2、t_role表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_role
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`role_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名',
`enable` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '是否可用:0-可用,1-不可用',
`create_time` datetime NOT NULL COMMENT '创建时间',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '创建人',
`update_time` datetime NOT NULL COMMENT '更新时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '更新人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_role
-- ----------------------------
INSERT INTO `t_role` VALUES (1, 'admin', '0', '2022-04-07 11:12:42', 'admin', '2022-04-07 11:12:49', 'admin');
INSERT INTO `t_role` VALUES (2, 'user', '0', '2022-04-07 14:28:48', 'admin', '2022-04-07 14:28:55', 'admin');
SET FOREIGN_KEY_CHECKS = 1;
4.3、t_user_role表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_user_role
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint NOT NULL COMMENT '用户ID,t_user表的主键ID',
`user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
`role_id` bigint NOT NULL COMMENT '角色ID,t_role表的主键ID',
`role_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名',
`enable` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '是否可用:0-可用,1-不可用',
`create_time` datetime NOT NULL COMMENT '创建时间',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '创建人',
`update_time` datetime NOT NULL COMMENT '更新时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '更新人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_user_role
-- ----------------------------
INSERT INTO `t_user_role` VALUES (1, 1, '张三', 1, 'admin', '0', '2022-04-07 11:11:21', 'admin', '2022-04-07 11:11:28', 'admin');
INSERT INTO `t_user_role` VALUES (2, 2, '李四', 2, 'user', '0', '2022-04-07 14:29:26', 'admin', '2022-04-07 14:29:32', 'admin');
SET FOREIGN_KEY_CHECKS = 1;
5、运行结果:
启动项目,浏览器输入http://127.0.0.1:8080/user/getUser?username=李四 回车。然后到达SpringSecurity提供的登陆页面。输入用户名李四,密码123321,点击登录,由于李四没有admin角色,所以会403,如图所示:
然后再次重启项目使用张三进行登录,浏览器输入http://127.0.0.1:8080/user/getUser?username=张三 回车,进行登录,用户名张三,密码123456如图,成功。
输入的明文密码SpringSecurity通过PasswordEncoder这个bean会对密码进行加密,然后和从数据库中查询的密码进行比对,一般的做法是在用户注册的时候,将密码进行加密,存储到数据库,我这没有注册,所以先把密码进行加密,直接存在了数据库。代码位于test包下,想给什么密码进行加密就替换一下"123321",如图输出的就是加密后的密码:
package mappertest;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class TestFindUserByUserName {
@Test
public void getEncodePassword(){
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode = bCryptPasswordEncoder.encode("123321");
System.out.println(encode);
}
}
6、一个问题:
使用PasswordEncoder对密码进行加密解密的时候,我一开始在数据库里存储的就是明文密码,想在UserRepositoryImpl里先把密码查出来,再进行加密,然后设置回内存中的user对象,于是在UserRepositoryImpl里引入了PasswordEncoder的bean,结果报错了,循环依赖,如图。然后我乖乖在数据库里存储了加密后的密码。