Spring全家桶-Spring Security之自定义数据库表认证和鉴权

2022年6月5日12:55:12

Spring全家桶-Spring Security之自定义数据库表认证和鉴权

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC(控制反转),DI(依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。



为什么需要自定义数据模型

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);}}

AbstractPasswordEncoderSpring Security提供的一个抽象,我们进行集成这个接口。或者也可以自己实现PasswordEncoder中的接口。

//对密码加密Stringencode(CharSequence rawPassword);//匹配密码,看看密码是否相等booleanmatches(CharSequence rawPassword,String encodedPassword);

我们可以通过实现PasswordEncoder中以上的两个方法进行自定义。如MD5,RSA等。

3.创建数据库对象实体

因为我们使用Mybatis进行数据库的访问。我们需要创建实体和数据库表中映射。我们现在创建了第一节中的三个表,我们创建三个实体如下:省略了getter和setter方法

  1. RoleInfo
publicclassRoleInfo{/**
     * 主键id
     */privateLong id;/**
     * 角色id
     */privateString roleName;/**
     * 用户id
     */privateString roleCode;/**
     * 用户id
     */privateDate createTime;}
  1. 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

  1. 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);}
  1. 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定义的几个方法:

  1. isAccountNonExpiredisAccountNonLockedisCredentialsNonExpired 暂且用不到, 统一返回true, 否则Spring Security会认为账号异常。
  2. isEnabled:对应enable字段, 将其代入即可。
  3. getAuthorities:方法是获取权限,我们这里是将角色和用户分开,考虑到角色和用户是多对多的关系,这里就需要一个中间表进行关系的维护。我们在buildGrantedAuthority进行构建权限数据,并设置到user中,提供给UserDetails使用。
  4. CustomeUserDetailService实现了UserDetailService,我们在之前中使用内存和jdbc的时候,都是实现了UserDetailService。对UserDetailService进行的扩展。
UserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException;
  1. 遇到的问题?
    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);}

因此我们需要手动加一下哦!这个也可以进行自定义扩展调整。我们后面在验证哦!????

  • 作者:Tony-devj
  • 原文链接:https://blog.csdn.net/qian1314520hu/article/details/124670799
    更新时间:2022年6月5日12:55:12 ,共 8531 字。