前面学习了Security简单的认证和授权,而我们使用的登录用户是在内存中或者配置文件中定义的,而实际项目中我们都是在数据库中定义用户,接下来我们开始学习如何将用户数据保存到数据库。
SpringSecurity支持多种不同的数据源,这些不同的数据源最终都将被封装成UserDetailsService的实例,可以自己封装,也可以使用系统默认提供的UserDetailsService实例,例如前面介绍的InMemoryUserDetailsManager;在UserDetailsService的实现类中,除了InMemoryUserDetailsManager之外,还有一个JdbcUserDetailsManager,使用JdbcUserDetailsManager可以让我们通过 JDBC 的方式将数据库和SpringSecurity连接起来,但是JdbcUserDetailsManager使用起来不是很方便,感兴趣的小伙伴可以自己去了解一下。
这里我们自己封装UserDetailsService实例,为了操作方便我们使用Mybatis-Plus来完成数据库操作,数据库使用H2Database,大家可以使用MySql都是一样的。
可以参考我这篇文章SpringBoot整合MyBatis-Plus+Druid,来整合SpringBoot、MyBatis-Plus、H2Database
创建新的SpringBoot项目,引入以下相关依赖
org.springframework.boot
spring-boot-starter-web
com.h2database
h2
runtime
org.projectlombok
lombok
true
com.alibaba
fastjson
1.2.70
com.baomidou
mybatis-plus-boot-starter
3.3.2
com.baomidou
mybatis-plus-generator
3.3.2
org.apache.velocity
velocity-engine-core
2.2
cn.hutool
hutool-all
5.3.5
配置application.properties配置文件
# DataSource Config
spring.datasource.driver-class-name=org.h2.Driver
# 配置本地数据库地址,按需修改
# DATABASE_TO_LOWER=TRUE;-要求数据库名称小写 CASE_INSENSITIVE_IDENTIFIERS=TRUE;-要求对字段的大小写不敏感
spring.datasource.url=jdbc:h2:D:/symon/project/db/security3;CASE_INSENSITIVE_IDENTIFIERS=TRUE;
spring.datasource.username=symon
spring.datasource.password=123456
# 指定Schema (DDL)脚本
spring.datasource.schema=classpath:db/schema.sql
# 指定Data (DML)脚本
spring.datasource.data=classpath:db/data.sql
# 指定schema要使用的Platform
spring.datasource.platform=h2
# 是否启用h2控制台
spring.h2.console.enabled=true
# 配置h2控制台访问地址,http://localhost:8080/h2
spring.h2.console.path=/h2
# MyBatis-Plus配置
mybatis-plus.mapper-locations=classpath:mapper/*.xml
在resources下创建db文件夹,并创建schema.sql和data.sql脚本,分别用来填写建表语句和初始化几条数据
#schema.sql
CREATE TABLE IF NOT EXISTS sys_user
(
ID bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
USERNAME varchar(50) NOT NULL COMMENT '用户名',
PASSWORD varchar(128) NOT NULL COMMENT '密码',
STATUS INT(2) NOT NULL COMMENT '状态 0锁定 1有效',
CREATE_TIME datetime(0) NOT NULL COMMENT '创建时间',
MODIFY_TIME datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (ID)
);
CREATE TABLE IF NOT EXISTS sys_role (
ID bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
ROLE_CODE varchar(100) NOT NULL COMMENT '角色标识',
ROLE_NAME varchar(100) NOT NULL COMMENT '角色名称',
CREATE_TIME datetime(0) NOT NULL COMMENT '创建时间',
MODIFY_TIME datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (ID)
);
CREATE TABLE IF NOT EXISTS sys_user_role (
ID bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户角色关联ID',
USER_ID bigint(20) NOT NULL COMMENT '用户ID',
ROLE_ID bigint(20) NOT NULL COMMENT '角色ID',
PRIMARY KEY (ID)
) ;
#data.sql
DELETE FROM sys_user;
INSERT INTO sys_user VALUES (1, 'symon', '$2a$10$Dm9tmMfvISVWTmrCM9WwUeSwgdwYSgU48zkqbhEcuDgl40SJuDYsu', 1, '2020-07-21 20:39:22', '2020-07-21 20:44:42');
INSERT INTO sys_user VALUES (2, 'test', '$2a$10$Dm9tmMfvISVWTmrCM9WwUeSwgdwYSgU48zkqbhEcuDgl40SJuDYsu', 1, '2020-07-21 20:39:22', '2020-07-21 20:44:42');
INSERT INTO sys_user VALUES (3, 'test1', '$2a$10$Dm9tmMfvISVWTmrCM9WwUeSwgdwYSgU48zkqbhEcuDgl40SJuDYsu', 0, '2020-07-21 20:39:22', '2020-07-21 20:44:42');
DELETE FROM sys_role;
INSERT INTO sys_role VALUES (1, 'root', '管理员', '2020-07-21 20:39:22', '2020-07-21 20:44:42');
INSERT INTO sys_role VALUES (2, 'user', '普通用户', '2020-07-21 20:39:22', '2020-07-21 20:44:42');
DELETE FROM sys_user_role;
INSERT INTO sys_user_role VALUES (1, 1, 1);
INSERT INTO sys_user_role VALUES (2, 2, 2);
INSERT INTO sys_user_role VALUES (3, 3, 2);
启动项目,之后访问登录h2控制台http://localhost:8080/h2,即可看到表已经创建成功
然后终止项目,使用MyBatis-Plus的代码生成器快速生成代码,可参考我这篇文章SpringBoot整合MyBatis-Plus+Druid
生成之后如下,我们可以看到实体类、mapper、service已经自动生成好了
创建SysUserDetails类实现UserDetails接口中的方法
public class SysUserDetails implements UserDetails {
private SysUserDTO user;
public SysUserDetails(SysUserDTO user) {
this.user = user;
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
List authorities = new ArrayList<>();
if (!CollectionUtils.isEmpty(user.getRoleList())) {
for (SysRole sysRole : user.getRoleList()) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + sysRole.getRoleCode()));
}
}
return authorities;
}
@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 Objects.equals(1, user.getStatus());}
}
其中:
SysUserDTO的内容如下:
@Data
public class SysUserDTO {
private Long id;
private String username;
private String password;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime modifyTime;
//用户关联角色
private List roleList;
}
UserDetails中有四个字段,accountNonExpired、accountNonLocked、credentialsNonExpired、enabled 这四个字段分别用来描述用户的状态,表示账户是否没有过期、账户是否没有被锁定、密码是否没有过期、以及账户是否可用。
getAuthorities 方法返回用户的角色信息,我们在这个方法中把自己的 Role 稍微转化一下即可。
创建SysUserDetailService实现UserDetailsService接口中的loadUserByUsername方法
@Service
public class SysUserDetailService implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUserDTO user = sysUserService.getUserDetail(username);
if (user == null){
throw new UsernameNotFoundException("用户名不存在!");
}
return new SysUserDetails(user);
}
}
其中:
SysUserService是刚刚自动生成的类,其中有一个手动创建的方法,其内容如下
@Service
public class SysUserServiceImpl extends ServiceImpl implements SysUserService {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private SysRoleMapper sysRoleMapper;
@Override
public SysUserDTO getUserDetail(String username) {
Wrapper wrapper = new QueryWrapper().eq("username", username);
SysUser sysUser = sysUserMapper.selectOne(wrapper);
if (sysUser != null) {
SysUserDTO user = BeanUtil.copyProperties(sysUser, SysUserDTO.class);
List roleList = sysRoleMapper.selectByUserId(sysUser.getId());
user.setRoleList(roleList);
return user;
} else {
return null;
}
}
}
getUserDetail方法中的SysRoleMapper及其映射xml文件的的方法的内容如下,该方法查询用户关联的角色
List selectByUserId(@Param("userId") Long userId);
UserDetailsService中的loadUserByUsername方法的参数就是用户在登录的时候传入的用户名,根据用户名去查询用户信息(查出来之后,系统会自动进行密码比对)
然后引入security依赖,并配置SecurityConfig,可以采用之前的配置,不过需要稍稍修改一下
org.springframework.boot
spring-boot-starter-security
SecurityConfig配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public static final String CONTENT_TYPE = "application/json;charset=utf-8";
@Autowired
private SysUserDetailService sysUserDetailService;
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);//BCryptPasswordEncoder加密
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(sysUserDetailService).passwordEncoder(passwordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/h2/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
.authorizeRequests()
.antMatchers("/user/**").hasRole("user")
.antMatchers("/root/**").hasRole("root")
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler((req, resp, auth) -> {
resp.setContentType(CONTENT_TYPE);
PrintWriter out = resp.getWriter();
out.write(JSON.toJSONString(ResponseDTO.success("登录成功!")));
out.flush();
out.close();
})
.failureHandler((req, resp, exception) -> {
resp.setContentType(CONTENT_TYPE);
PrintWriter out = resp.getWriter();
String exeMsg = "登录失败!";
if (exception instanceof LockedException) {
exeMsg = "账户已被锁定!";
} else if (exception instanceof CredentialsExpiredException) {
exeMsg = "密码已过期!";
} else if (exception instanceof AccountExpiredException) {
exeMsg = "账户已过期!";
} else if (exception instanceof DisabledException) {
exeMsg = "账户已被禁用!";
} else if (exception instanceof BadCredentialsException) {
exeMsg = "用户名或者密码输入错误,请重新输入!";
}
out.write(JSON.toJSONString(ResponseDTO.error(exeMsg)));
out.flush();
out.close();
})
.and()
.logout()
.logoutSuccessHandler((req, resp, auth) -> {
resp.setContentType(CONTENT_TYPE);
PrintWriter out = resp.getWriter();
out.write(JSON.toJSONString(ResponseDTO.success("注销成功!再见!")));
out.flush();
out.close();
})
.permitAll()
.and()
.exceptionHandling()
.authenticationEntryPoint((req, resp, exception) -> {
resp.setContentType(CONTENT_TYPE);
PrintWriter out = resp.getWriter();
out.write(JSON.toJSONString(ResponseDTO.unauthenticated("未登录,请重新登录!")));
out.flush();
out.close();
})
.accessDeniedHandler((req, resp, exception) -> {
resp.setContentType(CONTENT_TYPE);
PrintWriter out = resp.getWriter();
out.write(JSON.toJSONString(ResponseDTO.unauthorized("无权限!")));
out.flush();
out.close();
})
.and().csrf().disable();
}
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_root > ROLE_user");
return hierarchy;
}
}
其中:
configure(AuthenticationManagerBuilder auth)方法我们使用了自己定义的UserDetailService,并使用了BCryptPasswordEncoder将密码加密
启动之前,要创建测试类
@RestController
public class TestController {
@GetMapping("/test")
public String test() {
return "Hello World!";
}
@GetMapping("/user/test")
public String user(){
return "user权限";
}
@GetMapping("/root/test")
public String root(){
return "root权限";
}
}
启动类中要记得添加MapperScan
@SpringBootApplication
@MapperScan("com.example.security3.dao")
public class Security3Application {
public static void main(String[] args) {
SpringApplication.run(Security3Application.class, args);
}
}
然后启动项目,对接口进行测试
注意,这里我初始化的密码是123456加密之后的,可自行加密
public static void main(String[] args) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(10);
System.out.println(bCryptPasswordEncoder.encode("123456"));
}
另外test1用户在初始化时STATUS设置了0,在登陆验证时UserDetails的isEnabled返回了false,所以认为该账户被禁用,抛出DisabledException异常,被failureHandler捕获之后返回了自定义的结果。
@Override
public boolean isEnabled() {return Objects.equals(1, user.getStatus());}