Spring全家桶-Spring Security之自定义数据库表认证和鉴权
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC(控制反转),DI(依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
为什么需要自定义数据模型
Spring Security 默认提供了JDBC和内存进行管理多用户功能,但是默认的user.ddl的数据属性比较少,我们一般用户的属性有如用户名,邮件,真实姓名,手机号等。权限我们也不局限于相应的字段。一般我们都是通过id进行相关联,而默认是通过用户名相关联。有时候我们也需要修改JPA的框架,现在就来尝试一下。
一、自定义表结构
我们通过设计数据库来进行用户和角色的操作处理。数据库脚本如下:
CREATE TABLE `t_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(50) NOT NULL COMMENT '密码',
`enable` tinyint DEFAULT NULL COMMENT '是否可用',
`email` varchar(32) DEFAULT NULL COMMENT '邮件',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `t_role` (
`id` bigint NOT NULL COMMENT '主键',
`role_name` varchar(50) NOT NULL COMMENT '角色名称',
`role_code` varchar(50) NOT NULL COMMENT '角色编码',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `t_user_role` (
`id` bigint NOT NULL COMMENT '主键',
`role_id` bigint NOT NULL,
`user_id` bigint NOT NULL,
PRIMARY KEY (`id`),
KEY `role_id` (`role_id`),
KEY `user_id` (`user_id`),
CONSTRAINT `role_id` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`),
CONSTRAINT `user_id` FOREIGN KEY (`user_id`) REFERENCES `t_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
二、使用mybatis 进行数据库操作
1.搭建环境
创建项目:spring-security-custome-datastruct 项目的完整的POM:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
2.修改配置类WebSecurityConfig
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomeUserDetailService customeUserDetailService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customeUserDetailService).passwordEncoder(new CustomePasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/books/**").hasAnyRole("ADMIN")
.antMatchers("/user/**").hasAnyRole("ADMIN","USER")
.antMatchers("/").permitAll()
.and().formLogin().loginPage("/login.html").permitAll().and().csrf().disable();
}
}
CustomePasswordEncoder 是自定义密码加密策略,这里我们不进行任何的加密策略。直接使用数据库中的密码进行登录。
public class CustomePasswordEncoder extends AbstractPasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
@Override
protected byte[] encode(CharSequence rawPassword, byte[] salt) {
return new byte[0];
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return StringUtils.endsWithIgnoreCase(rawPassword.toString(),encodedPassword);
}
}
AbstractPasswordEncoder 是Spring Security 提供的一个抽象,我们进行集成这个接口。或者也可以自己实现PasswordEncoder 中的接口。
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
我们可以通过实现PasswordEncoder 中以上的两个方法进行自定义。如MD5,RSA等。
3.创建数据库对象实体
因为我们使用Mybatis 进行数据库的访问。我们需要创建实体和数据库表中映射。我们现在创建了第一节中的三个表,我们创建三个实体如下:省略了getter和setter方法
- RoleInfo
public class RoleInfo {
private Long id;
private String roleName;
private String roleCode;
private Date createTime;
}
- UserInfo:
public class UserInfo implements UserDetails {
private Long id;
private String username;
private String password;
private Integer enable;
private String email;
private Date createTime;
private List<GrantedAuthority> authorities;
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.enable == 1;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
public void setAuthorities(List<GrantedAuthority> authorities){
this.authorities = authorities;
}
}
3.创建数据库访问DAO
- RoleInfoDao
@Repository
public interface RoleInfoDao {
@Select("<script>select * from t_role where id in (" +
"<foreach index='index' collection='ids' separator=',' item='item'>#{item}</foreach>" +
")</script>")
List<RoleInfo> getByIds(@Param("ids") List<Long> ids);
}
@Repository
public interface RoleUserInfoDao {
@Select("select * from t_user_role where user_id = #{userId}")
List<RoleUserInfo> getRoleUserByUserId(@Param("userId") Long userId);
}
- UserInfoDao
@Repository
public interface UserInfoDao {
@Select("select * from t_user where username = #{username}")
UserInfo findUserByUsername(@Param("username") String username);
}
4.创建自定义CustomeUserDetailService
CustomeUserDetailService 进行用户的查询和权限构建操作。
@Service
public class CustomeUserDetailService implements UserDetailsService {
@Autowired
private UserInfoDao userInfoDao;
@Autowired
private RoleInfoDao roleInfoDao;
@Autowired
private RoleUserInfoDao roleUserInfoDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserInfo userInfo = userInfoDao.findUserByUsername(username);
if(Objects.isNull(userInfo)){
throw new UsernameNotFoundException("用户不存在");
}
List<GrantedAuthority> authorities = buildGrantedAuthority(userInfo.getId());
userInfo.setAuthorities(authorities);
return userInfo;
}
private List<GrantedAuthority> buildGrantedAuthority(Long id) {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
List<RoleUserInfo> roleUserInfos = roleUserInfoDao.getRoleUserByUserId(id);
List<Long> roleIds = roleUserInfos.stream().map(RoleUserInfo::getId).collect(Collectors.toList());
List<RoleInfo> roleInfos = roleInfoDao.getByIds(roleIds);
List<String> roleCodes = roleInfos.stream().map(RoleInfo::getRoleCode).collect(Collectors.toList());
roleCodes.forEach(roleCode -> grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + roleCode)));
return grantedAuthorities;
}
}
5.调整配置与启动类
spring:
datasource:
password: 数据库密码
username: 数据库用户名
url: jdbc:mysql:///spring-security-learn?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
我们到这里基本上就实现了自定义数据库表的认证和鉴权。用户的现在和角色的新增可以自行提供相关的接口进行数据的维护,这里就不细说了。 启动类:CustomeDataStructApplication
@SpringBootApplication
@MapperScan(basePackages = "org.tony.spring.security.dao")
public class CustomeDataStructApplication {
public static void main(String[] args) {
SpringApplication.run(CustomeDataStructApplication.class,args);
}
}
@MapperScan :进行数据访问的包扫描,自动装载。
6.运行项目
可以和之前文章中一样的启动应用程序,程序将能正常启动,权限能正常拦截。
总结
我们这里使用自定义的数据库权限的时候,用户对象是实现了UserDetail ,实现UserDetails定义的几个方法:
isAccountNonExpired 、 isAccountNonLocked 和 isCredentialsNonExpired 暂且用不到, 统一返回true, 否则Spring Security会认为账号异常。- isEnabled:对应enable字段, 将其代入即可。
- getAuthorities:方法是获取权限,我们这里是将角色和用户分开,考虑到角色和用户是多对多的关系,这里就需要一个中间表进行关系的维护。我们在
buildGrantedAuthority 进行构建权限数据,并设置到user中,提供给UserDetails使用。 CustomeUserDetailService 实现了UserDetailService,我们在之前中使用内存和jdbc的时候,都是实现了UserDetailService。对UserDetailService进行的扩展。
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
- 遇到的问题?
A. 没有映射到PasswordEncoder ,是由于没有设置PasswordEncoder 导致,所以我们自定义了一个PasswordEncoder 。 B. 登陆之后,访问报403?
roleCodes.forEach(roleCode -> grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + roleCode)));
是由于Spring Security 默认会有一个ROLE_ 的前缀.
public ExpressionUrlAuthorizationConfigurer<H>.ExpressionInterceptUrlRegistry hasAnyRole(String... roles) {
return this.access(ExpressionUrlAuthorizationConfigurer.hasAnyRole(ExpressionUrlAuthorizationConfigurer.this.rolePrefix, roles));
}
this.rolePrefix:就是角色的前缀。
public ExpressionUrlAuthorizationConfigurer(ApplicationContext context) {
String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class);
if (grantedAuthorityDefaultsBeanNames.length == 1) {
GrantedAuthorityDefaults grantedAuthorityDefaults = (GrantedAuthorityDefaults)context.getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class);
this.rolePrefix = grantedAuthorityDefaults.getRolePrefix();
} else {
this.rolePrefix = "ROLE_";
}
this.REGISTRY = new ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry(context);
}
因此我们需要手动加一下哦!这个也可以进行自定义扩展调整。我们后面在验证哦!😁
|