文章目录
- SpringSecurity基于数据库的认证与授权
- 一、自定义用户信息UserDetails
- 1.1 新建用户信息类UserDetails
- 1.2 UserDetailsService
- 二、基于数据库的认证
- 2.1 连接数据库
- 2.2 获取用户信息
- 2.2.1 获取用户实体类
- 2.2.2 Mapper
- 2.2.3 Service
- 2.3 认证
- 2.3.1 实现UserDetails接口
- 2.3.2 实现UserDetailsService接口
- 2.3.3 安全配置类
- 2.3.4 测试程序
- 2.3.4.1 控制器
- 3.3.4.1 访问
- 三、基于数据库的授权
- 3.1 数据库表
- 3.2 获取权限
- 3.2.1 权限类 SysMenu
- 3.2.2 SysMenuDao
- 3.2.3 SysMenuServiceImpl
- 3.2.4 修改 UserDetailsService
- 3.2.5 修改 UserDetails
- 3.3 测试
- 四、总结
SpringSecurity基于数据库的认证与授权
承接:2.SpringSecurity - 处理器简单说明-CSDN博客
我们之前学习的用户的信息都是配置在代码中,如下段代码所示
/*** 定义一个Bean,用户详情服务接口* <p>* 系统中默认是有这个UserDetailsService的,也就是默认的用户名(user)和默认密码(控制台生成的)* 如果在yaml文件中配置了用户名和密码,那在系统中的就是yaml文件中的信息* <p>* 我们自定义了之后,就会把系统中的UserDetailsService覆盖掉*/
@Configuration
public class MySecurityUserConfig {/*** 根据用户名把用户的详情从数据库中获取出来,封装成用户细节信息UserDetails(包括用户名、密码、用户所拥有的权限)* <p>* UserDetails存储的是用户的用户名、密码、去权限信息*/@Beanpublic UserDetailsService userDetailsService() {
// 用户细节信息,创建两个用户
// 此User是SpringSecurity框架中的public class User implements UserDetails, CredentialsContainerUserDetails user1 = User.builder().username("zhangjingqi-1").password(passwordEncoder().encode("123456"))
// 配置用户角色.roles("student") //角色到系统中会变成权限的,比如这里会变成ROLE_student,ROLE_manager.build();UserDetails user2 = User.builder().username("zhangjingqi-2").password(passwordEncoder().encode("123456"))
// 配置权限.authorities("teacher:query").build();UserDetails user3 = User.builder().username("admin").password(passwordEncoder().encode("123456"))
// 配置权限.authorities("teacher:query","teacher:add","teacher:update","teacher:delete").build();// InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService,其中UserDetailsManager继承UserDetailsServiceInMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();userDetailsManager.createUser(user1);userDetailsManager.createUser(user2);userDetailsManager.createUser(user3);return userDetailsManager;}/*** 配置密码加密器* NoOpPasswordEncoder.getInstance() 此实例表示不加密* BCryptPasswordEncoder() 会加密*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}}
但是我们并不希望上面这样写,我们希望把用户的信息存入到数据库
一、自定义用户信息UserDetails
用户实体类需要实现UserDetails接口,并实现该接口中的7个方法, UserDetails 接口的7个方法如下图
1.1 新建用户信息类UserDetails
//只有当"accountNonExpired"、“accountNonLocked”、“credentialsNonExpired”、"enabled"都为true时,账户才能使用
//之前我们创建的时候,直接User.builder()创建,之后InMemoryUserDetailsManager对象createUser
public class SecurityUser implements UserDetails {/*** @return 权限信息*/@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}/*** @return 用户密码,一定是加密后的密码*/@Overridepublic String getPassword() {//明文为123456return "$2a$10$KyXAnVcsrLaHMWpd3e2xhe6JmzBi.3AgMhteFq8t8kjxmwL8olEDq";}/*** @return 用户名*/@Overridepublic String getUsername() {return "thomas";}/*** @return 账户是否过期*/@Overridepublic boolean isAccountNonExpired() {return true;}/*** @return 账户是否被锁住*/@Overridepublic boolean isAccountNonLocked() {return true;}/*** @return 凭据是否过期*/@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** @return 账户是否可用*/@Overridepublic boolean isEnabled() {return true;}
}
1.2 UserDetailsService
/*** 当我们定义了此类后,系统默认的UserDetailsService不会起作用,下面UserServiceImpl会起作用*/
@Service
public class UserServiceImpl implements UserDetailsService {/*** 根据用户名获取用户详情UserDetails*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {SecurityUser securityUser= new SecurityUser();//我们自己定义的if(username==null || !username.equals(securityUser.getUsername())){
// SpringSecurity框架中自带的异常throw new UsernameNotFoundException("该用户不存在或用户名不正确");}
// 执行到这里,说明username是没有问题的
// 用户密码对不对,框架会帮我们进行判断return securityUser;}}
二、基于数据库的认证
我们观察到在1.1UserDetails中,我们把用户名和密码是写死的,但是这种情况下是不合理的,包括权限在内,我们都需要从数据库中取出来
将信息从数据库取出来后,可以将信息封装成一个UserDetails类
2.1 连接数据库
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId>
</dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.15</version>
</dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.1</version>
</dependency>
spring:#数据源datasource:#德鲁伊连接池druid:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/springsecurity?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=trueusername: rootpassword: rootmybatis:#SQL映射文件的位置mapper-locations: classpath:mapper/**/*.xml# 指定实体类起别名,(实体类所在的包的包路径,那么包中的所有实体类别名就默认是类名首字母小写)type-aliases-package: com.zhangjingqi.entityconfiguration:#开启驼峰命名法map-underscore-to-camel-case: true#日志功能log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
2.2 获取用户信息
判断用户是否正确,我们可以从sys_user中取出用户相关信息,将用户的相关信息取出来后封装成一个UserDetails类
2.2.1 获取用户实体类
获取用户信息的实体类,这里不建议SysUser实体类实现UserDetails接口,因为会显得很乱
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SysUser implements Serializable {private static final long serialVersionUID = -5352627792860514242L;private Integer userId;private String username;private String password;private String sex;private String address;private Integer enabled;private Integer accountNoExpired;private Integer credentialsNoExpired;private Integer accountNoLocked;}
2.2.2 Mapper
封装dao层,也就是Mapper层,从数据库中获取用户的信息
@Mapper
public interface SysUserDao {/*** 根据用户名访问用户信息* @param userName 用户名* @return 用户信息*/SysUser getByUserName(@Param("userName") String userName);}
<mapper namespace="com.zhangjingqi.dto.SysUserDao"><select id="getByUserName" resultType="com.zhangjingqi.entity.SysUser">select user_id,username,password,sex,address,enabled,account_no_expired,credentials_no_expired,account_no_lockedfrom sys_user where username = #{userName};</select></mapper>
2.2.3 Service
@Slf4j
@Service
public class SysUserServiceImpl implements SysUserService {@Autowiredprivate SysUserDao sysUserDao;@Overridepublic SysUser getByUserName(String userName) {return sysUserDao.getByUserName(userName);}
}
2.3 认证
2.3.1 实现UserDetails接口
我们之前是写死的用户信息,但是现在是从数据库中进行获取的
@Data
public class SecurityUser implements UserDetails {private static final long serialVersionUID = -1314948905954698478L;private final SysUser sysUser ;public SecurityUser(SysUser sysUser) {this.sysUser = sysUser;}/*** @return 权限信息*/@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}/*** @return 用户密码,一定是加密后的密码*/@Overridepublic String getPassword() {return sysUser.getPassword();}/*** @return 用户名*/@Overridepublic String getUsername() {return sysUser.getUsername();}/*** @return 账户是否过期*/@Overridepublic boolean isAccountNonExpired() {return sysUser.getAccountNoExpired() != 0;}/*** @return 账户是否被锁住*/@Overridepublic boolean isAccountNonLocked() {return sysUser.getAccountNoLocked() !=0;}/*** @return 凭据是否过期*/@Overridepublic boolean isCredentialsNonExpired() {return sysUser.getCredentialsNoExpired() !=0 ;}/*** @return 账户是否可用*/@Overridepublic boolean isEnabled() {return sysUser.getEnabled() !=0 ;}
}
这个地方我们之前是这么写的
2.3.2 实现UserDetailsService接口
@Slf4j
@Service
public class SecurityUserDetailsService implements UserDetailsService {@Autowiredprivate SysUserService sysUserService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1.从数据库获取用户的详情信息SysUser sysUser = sysUserService.getByUserName(username);if (null == sysUser){
// 这个异常信息是SpringSecurity中封装的throw new UsernameNotFoundException("用户没有找到");}// 2.封装成UserDetails类,SecurityUser类实现了UserDetails接口SecurityUser securityUser = new SecurityUser(sysUser);return securityUser;}
}
我们之前是这么写的
这篇文章会有介绍:1.SpringSecurity -快速入门、加密、基础授权-CSDN博客
2.3.3 安全配置类
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {/*** 重写 configure(HttpSecurity http)方法*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()//授权http请求.anyRequest()//任何请求.authenticated();//需要验证http.formLogin().permitAll(); //SpringSecurity的表单认证}/*** @return 密码加密器*/@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}}
2.3.4 测试程序
2.3.4.1 控制器
@RestController
@RequestMapping("/student")
public class StudentController {@GetMapping("/query")private String query(){return "query student";}@GetMapping("/add")private String add(){return "add student";}@GetMapping("/delete")private String delete(){return "delete student";}@GetMapping("/update")private String update(){return "export student";}
}
@RestController
@RequestMapping("/teacher")
public class TeacherController {@GetMapping("/query")@PreAuthorize("hasAuthority('teacher:query')")//预授权public String queryInfo() {return "teacher query";}}
3.3.4.1 访问
用下面这个人的信息进行登录
访问一下localhost:8080/student/query,发现可以访问
再访问一下localhost:8080/teacher/query,发现是不能访问的,原因是thomas用户没有/teacher/query路径的权限
三、基于数据库的授权
3.1 数据库表
首先我们很明确的就是用户和角色的关系是多对多的
用户表
角色表
用户与角色关联表
权限表
权限与角色关联表
3.2 获取权限
3.2.1 权限类 SysMenu
@Data
public class SysMenu implements Serializable {private static final long serialVersionUID = 597868207552115176L;private Integer id;private Integer pid;private Integer type;private String name;private String code;
}
3.2.2 SysMenuDao
@Mapper
public interface SysMenuDao {List<String> queryPermissionByUserId(@Param("userId") Integer userId);
}
这个地方涉及到三张表,角色用户关联表sys_role_user、角色权限关联表sys_role_menu、权限表sys_menu
我们要实现通过用户获取对应的权限
<mapper namespace="com.zhangjingqi.dto.SysMenuDao"><select id="queryPermissionByUserId" resultType="java.lang.String">SELECT distinct sm.codeFROM sys_role_user sruinner join sys_role_menu srmon sru.rid = srm.ridinner join sys_menu sm on srm.mid = sm.idwhere sru.uid = #{userId}and sm.delete_flag = 0</select>
</mapper>
3.2.3 SysMenuServiceImpl
@Service
public class SysMenuServiceImpl implements SysMenuService {@Autowiredprivate SysMenuDao sysMenuDao;@Overridepublic List<String> queryPermissionByUserId(Integer userId) {return sysMenuDao.queryPermissionByUserId(userId);}
}
3.2.4 修改 UserDetailsService
@Slf4j
@Service
public class SecurityUserDetailsService implements UserDetailsService {@Autowiredprivate SysUserService sysUserService;@Autowiredprivate SysMenuService sysMenuService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1.从数据库获取用户的详情信息SysUser sysUser = sysUserService.getByUserName(username);if (null == sysUser){
// 这个异常信息是SpringSecurity中封装的throw new UsernameNotFoundException("用户没有找到");}// 2.获取该用户的权限List<String> permissionList = sysMenuService.queryPermissionByUserId(sysUser.getUserId());// List<SimpleGrantedAuthority> simpleGrantedAuthorities = permissionList.stream().map(permission -> new SimpleGrantedAuthority(permission) ).collect(Collectors.toList());
// 将集合的泛型转换成SimpleGrantedAuthority (GrantedAuthority类的子类即可)List<SimpleGrantedAuthority> simpleGrantedAuthorities = permissionList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());// 2.封装成UserDetails类,SecurityUser类实现了UserDetails接口SecurityUser securityUser = new SecurityUser(sysUser);securityUser.setSimpleGrantedAuthorities(simpleGrantedAuthorities);return securityUser;}
}
3.2.5 修改 UserDetails
@Data
public class SecurityUser implements UserDetails {private static final long serialVersionUID = -1314948905954698478L;private final SysUser sysUser ;// 用户权限private List<SimpleGrantedAuthority> simpleGrantedAuthorities;public SecurityUser(SysUser sysUser) {this.sysUser = sysUser;}/*** 这个集合中对象的类型必须是GrantedAuthority类或其子类* @return 权限信息*/@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return simpleGrantedAuthorities;}/*** @return 用户密码,一定是加密后的密码*/@Overridepublic String getPassword() {return sysUser.getPassword();}/*** @return 用户名*/@Overridepublic String getUsername() {return sysUser.getUsername();}/*** @return 账户是否过期*/@Overridepublic boolean isAccountNonExpired() {return sysUser.getAccountNoExpired() != 0;}/*** @return 账户是否被锁住*/@Overridepublic boolean isAccountNonLocked() {return sysUser.getAccountNoLocked() !=0;}/*** @return 凭据是否过期*/@Overridepublic boolean isCredentialsNonExpired() {return sysUser.getCredentialsNoExpired() !=0 ;}/*** @return 账户是否可用*/@Overridepublic boolean isEnabled() {return sysUser.getEnabled() !=0 ;}
}
3.3 测试
使用Obama登录
查看obama权限
四、总结
认证与授权,需要我们实现UserDetails用户详情类和UserDetailsService类