Spring全家桶-Spring Security之自定义数据库表认证和鉴权
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC(控制反转),DI(依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
文章目录
- Spring全家桶-Spring Security之自定义数据库表认证和鉴权
- 为什么需要自定义数据模型
- 一、自定义表结构
- 二、使用`mybatis`进行数据库操作
-
- 1.搭建环境
- 2.修改配置类`WebSecurityConfig`
- 3.创建数据库对象实体
- 3.创建数据库访问DAO
- 4.创建自定义CustomeUserDetailService
- 5.调整配置与启动类
- 6.运行项目
- 总结
为什么需要自定义数据模型
Spring Security
默认提供了JDBC和内存进行管理多用户功能,但是默认的user.ddl的数据属性比较少,我们一般用户的属性有如用户名,邮件,真实姓名,手机号等。权限我们也不局限于相应的字段。一般我们都是通过id进行相关联,而默认是通过用户名相关联。有时候我们也需要修改JPA的框架,现在就来尝试一下。
一、自定义表结构
我们通过设计数据库来进行用户和角色的操作处理。数据库脚本如下:
-- 用户表CREATETABLE`t_user`(`id`bigintNOTNULLAUTO_INCREMENTCOMMENT'主键',`username`varchar(50)NOTNULLCOMMENT'用户名',`password`varchar(50)NOTNULLCOMMENT'密码',`enable`tinyintDEFAULTNULLCOMMENT'是否可用',`email`varchar(32)DEFAULTNULLCOMMENT'邮件',`create_time`datetimeDEFAULTNULLCOMMENT'创建时间',PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_0900_ai_ci;-- 角色表CREATETABLE`t_role`(`id`bigintNOTNULLCOMMENT'主键',`role_name`varchar(50)NOTNULLCOMMENT'角色名称',`role_code`varchar(50)NOTNULLCOMMENT'角色编码',`create_time`datetimeDEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_0900_ai_ci;-- 创建用户角色关联表CREATETABLE`t_user_role`(`id`bigintNOTNULLCOMMENT'主键',`role_id`bigintNOTNULL,`user_id`bigintNOTNULL,PRIMARYKEY(`id`),KEY`role_id`(`role_id`),KEY`user_id`(`user_id`),CONSTRAINT`role_id`FOREIGNKEY(`role_id`)REFERENCES`t_role`(`id`),CONSTRAINT`user_id`FOREIGNKEY(`user_id`)REFERENCES`t_user`(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=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
@EnableWebSecuritypublicclassWebSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredprivateCustomeUserDetailService customeUserDetailService;@Overrideprotectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{
auth.userDetailsService(customeUserDetailService).passwordEncoder(newCustomePasswordEncoder());}@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{
http.authorizeRequests().antMatchers("/books/**").hasAnyRole("ADMIN").antMatchers("/user/**").hasAnyRole("ADMIN","USER").antMatchers("/").permitAll().and().formLogin().loginPage("/login.html").permitAll().and().csrf().disable();}}
CustomePasswordEncoder
是自定义密码加密策略,这里我们不进行任何的加密策略。直接使用数据库中的密码进行登录。
publicclassCustomePasswordEncoderextendsAbstractPasswordEncoder{@OverridepublicStringencode(CharSequence rawPassword){return rawPassword.toString();}@Overrideprotectedbyte[]encode(CharSequence rawPassword,byte[] salt){returnnewbyte[0];}@Overridepublicbooleanmatches(CharSequence rawPassword,String encodedPassword){returnStringUtils.endsWithIgnoreCase(rawPassword.toString(),encodedPassword);}}
AbstractPasswordEncoder
是Spring Security
提供的一个抽象,我们进行集成这个接口。或者也可以自己实现PasswordEncoder
中的接口。
//对密码加密Stringencode(CharSequence rawPassword);//匹配密码,看看密码是否相等booleanmatches(CharSequence rawPassword,String encodedPassword);
我们可以通过实现PasswordEncoder
中以上的两个方法进行自定义。如MD5,RSA等。
3.创建数据库对象实体
因为我们使用Mybatis
进行数据库的访问。我们需要创建实体和数据库表中映射。我们现在创建了第一节中的三个表,我们创建三个实体如下:省略了getter和setter方法
- RoleInfo
publicclassRoleInfo{/**
* 主键id
*/privateLong id;/**
* 角色id
*/privateString roleName;/**
* 用户id
*/privateString roleCode;/**
* 用户id
*/privateDate createTime;}
- UserInfo:
publicclassUserInfoimplementsUserDetails{/**
* 主键id
*/privateLong id;/**
* 用户名
*/privateString username;/**
* 密码
*/privateString password;/**
* 是否可用
*/privateInteger enable;/**
* 邮件
*/privateString email;/**
* 创建时间
*/privateDate createTime;/**
* 权限
*/privateList<GrantedAuthority> authorities;@OverridepublicbooleanisAccountNonExpired(){returntrue;}@OverridepublicbooleanisAccountNonLocked(){returntrue;}@OverridepublicbooleanisCredentialsNonExpired(){returntrue;}@OverridepublicbooleanisEnabled(){returnthis.enable==1;}@OverridepublicCollection<?extendsGrantedAuthority>getAuthorities(){returnthis.authorities;}publicvoidsetAuthorities(List<GrantedAuthority> authorities){this.authorities= authorities;}}
3.创建数据库访问DAO
- RoleInfoDao
@RepositorypublicinterfaceRoleInfoDao{//通过角色id批量查询角色信息@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);}
@RepositorypublicinterfaceRoleUserInfoDao{//通过用户id查询关联的角色id@Select("select * from t_user_role where user_id = #{userId}")List<RoleUserInfo>getRoleUserByUserId(@Param("userId")Long userId);}
- UserInfoDao
@RepositorypublicinterfaceUserInfoDao{//通过用户名查询用户@Select("select * from t_user where username = #{username}")UserInfofindUserByUsername(@Param("username")String username);}
4.创建自定义CustomeUserDetailService
CustomeUserDetailService
进行用户的查询和权限构建操作。
@ServicepublicclassCustomeUserDetailServiceimplementsUserDetailsService{@AutowiredprivateUserInfoDao userInfoDao;@AutowiredprivateRoleInfoDao roleInfoDao;@AutowiredprivateRoleUserInfoDao roleUserInfoDao;@OverridepublicUserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException{UserInfo userInfo= userInfoDao.findUserByUsername(username);if(Objects.isNull(userInfo)){thrownewUsernameNotFoundException("用户不存在");}List<GrantedAuthority> authorities=buildGrantedAuthority(userInfo.getId());
userInfo.setAuthorities(authorities);return userInfo;}/**
* 组装权限
* @param id 用户id
* @return 返回用户权限信息
*/privateList<GrantedAuthority>buildGrantedAuthority(Long id){List<GrantedAuthority> grantedAuthorities=newArrayList<>();//查询用户所有的权限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(newSimpleGrantedAuthority("ROLE_"+ roleCode)));return grantedAuthorities;}}
5.调整配置与启动类
spring:datasource:password: 数据库密码username: 数据库用户名url: jdbc:mysql:///spring-security-learn?characterEncoding=UTF-8&serverTimezone=Asia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Drivermybatis:configuration:map-underscore-to-camel-case:true#设置驼峰log-impl: org.apache.ibatis.logging.stdout.StdOutImpl#进行sql的打印
我们到这里基本上就实现了自定义数据库表的认证和鉴权。用户的现在和角色的新增可以自行提供相关的接口进行数据的维护,这里就不细说了。
启动类:CustomeDataStructApplication
@SpringBootApplication@MapperScan(basePackages="org.tony.spring.security.dao")publicclassCustomeDataStructApplication{publicstaticvoidmain(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进行的扩展。
UserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException;
- 遇到的问题?
A. 没有映射到PasswordEncoder
,是由于没有设置PasswordEncoder
导致,所以我们自定义了一个PasswordEncoder
。
B. 登陆之后,访问报403?
roleCodes.forEach(roleCode-> grantedAuthorities.add(newSimpleGrantedAuthority("ROLE_"+ roleCode)));
是由于Spring Security
默认会有一个ROLE_
的前缀.
publicExpressionUrlAuthorizationConfigurer<H>.ExpressionInterceptUrlRegistryhasAnyRole(String... roles){returnthis.access(ExpressionUrlAuthorizationConfigurer.hasAnyRole(ExpressionUrlAuthorizationConfigurer.this.rolePrefix, roles));}
this.rolePrefix:就是角色的前缀。
publicExpressionUrlAuthorizationConfigurer(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=newExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry(context);}
因此我们需要手动加一下哦!这个也可以进行自定义扩展调整。我们后面在验证哦!????