文章目录
- SpringSecurity基于数据库的认证与授权
- 一、自定义用户信息UserDetails
-
- [1.1 新建用户信息类UserDetails](#1.1 新建用户信息类UserDetails)
- [1.2 UserDetailsService](#1.2 UserDetailsService)
- 二、基于数据库的认证
-
- [2.1 连接数据库](#2.1 连接数据库)
- [2.2 获取用户信息](#2.2 获取用户信息)
-
- [2.2.1 获取用户实体类](#2.2.1 获取用户实体类)
- [2.2.2 Mapper](#2.2.2 Mapper)
- [2.2.3 Service](#2.2.3 Service)
- [2.3 认证](#2.3 认证)
-
- [2.3.1 实现UserDetails接口](#2.3.1 实现UserDetails接口)
- [2.3.2 实现UserDetailsService接口](#2.3.2 实现UserDetailsService接口)
- [2.3.3 安全配置类](#2.3.3 安全配置类)
- [2.3.4 测试程序](#2.3.4 测试程序)
-
- [2.3.4.1 控制器](#2.3.4.1 控制器)
- [3.3.4.1 访问](#3.3.4.1 访问)
- 三、基于数据库的授权
-
- [3.1 数据库表](#3.1 数据库表)
- [3.2 获取权限](#3.2 获取权限)
-
- [3.2.1 权限类 SysMenu](#3.2.1 权限类 SysMenu)
- [3.2.2 SysMenuDao](#3.2.2 SysMenuDao)
- [3.2.3 SysMenuServiceImpl](#3.2.3 SysMenuServiceImpl)
- [3.2.4 修改 UserDetailsService](#3.2.4 修改 UserDetailsService)
- [3.2.5 修改 UserDetails](#3.2.5 修改 UserDetails)
- [3.3 测试](#3.3 测试)
- 四、总结
SpringSecurity基于数据库的认证与授权
我们之前学习的用户的信息都是配置在代码中,如下段代码所示
java
/**
* 定义一个Bean,用户详情服务接口
* <p>
* 系统中默认是有这个UserDetailsService的,也就是默认的用户名(user)和默认密码(控制台生成的)
* 如果在yaml文件中配置了用户名和密码,那在系统中的就是yaml文件中的信息
* <p>
* 我们自定义了之后,就会把系统中的UserDetailsService覆盖掉
*/
@Configuration
public class MySecurityUserConfig {
/**
* 根据用户名把用户的详情从数据库中获取出来,封装成用户细节信息UserDetails(包括用户名、密码、用户所拥有的权限)
* <p>
* UserDetails存储的是用户的用户名、密码、去权限信息
*/
@Bean
public UserDetailsService userDetailsService() {
// 用户细节信息,创建两个用户
// 此User是SpringSecurity框架中的public class User implements UserDetails, CredentialsContainer
UserDetails 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继承UserDetailsService
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(user1);
userDetailsManager.createUser(user2);
userDetailsManager.createUser(user3);
return userDetailsManager;
}
/**
* 配置密码加密器
* NoOpPasswordEncoder.getInstance() 此实例表示不加密
* BCryptPasswordEncoder() 会加密
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
但是我们并不希望上面这样写,我们希望把用户的信息存入到数据库
一、自定义用户信息UserDetails
用户实体类需要实现UserDetails接口,并实现该接口中的7个方法, UserDetails 接口的7个方法如下图
1.1 新建用户信息类UserDetails
java
//只有当"accountNonExpired"、"accountNonLocked"、"credentialsNonExpired"、"enabled"都为true时,账户才能使用
//之前我们创建的时候,直接User.builder()创建,之后InMemoryUserDetailsManager对象createUser
public class SecurityUser implements UserDetails {
/**
* @return 权限信息
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
/**
* @return 用户密码,一定是加密后的密码
*/
@Override
public String getPassword() {
//明文为123456
return "$2a$10$KyXAnVcsrLaHMWpd3e2xhe6JmzBi.3AgMhteFq8t8kjxmwL8olEDq";
}
/**
* @return 用户名
*/
@Override
public String getUsername() {
return "thomas";
}
/**
* @return 账户是否过期
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* @return 账户是否被锁住
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* @return 凭据是否过期
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* @return 账户是否可用
*/
@Override
public boolean isEnabled() {
return true;
}
}
1.2 UserDetailsService
java
/**
* 当我们定义了此类后,系统默认的UserDetailsService不会起作用,下面UserServiceImpl会起作用
*/
@Service
public class UserServiceImpl implements UserDetailsService {
/**
* 根据用户名获取用户详情UserDetails
*/
@Override
public 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 连接数据库
xml
<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>
yaml
spring:
#数据源
datasource:
#德鲁伊连接池
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springsecurity?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
mybatis:
#SQL映射文件的位置
mapper-locations: classpath:mapper/**/*.xml
# 指定实体类起别名,(实体类所在的包的包路径,那么包中的所有实体类别名就默认是类名首字母小写)
type-aliases-package: com.zhangjingqi.entity
configuration:
#开启驼峰命名法
map-underscore-to-camel-case: true
#日志功能
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
2.2 获取用户信息
判断用户是否正确,我们可以从sys_user中取出用户相关信息,将用户的相关信息取出来后封装成一个UserDetails类
2.2.1 获取用户实体类
获取用户信息的实体类,这里不建议SysUser实体类实现UserDetails接口,因为会显得很乱
java
@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层,从数据库中获取用户的信息
java
@Mapper
public interface SysUserDao {
/**
* 根据用户名访问用户信息
* @param userName 用户名
* @return 用户信息
*/
SysUser getByUserName(@Param("userName") String userName);
}
sql
<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_locked
from sys_user
where username = #{userName};
</select>
</mapper>
2.2.3 Service
java
@Slf4j
@Service
public class SysUserServiceImpl implements SysUserService {
@Autowired
private SysUserDao sysUserDao;
@Override
public SysUser getByUserName(String userName) {
return sysUserDao.getByUserName(userName);
}
}
2.3 认证
2.3.1 实现UserDetails接口
我们之前是写死的用户信息,但是现在是从数据库中进行获取的
java
@Data
public class SecurityUser implements UserDetails {
private static final long serialVersionUID = -1314948905954698478L;
private final SysUser sysUser ;
public SecurityUser(SysUser sysUser) {
this.sysUser = sysUser;
}
/**
* @return 权限信息
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
/**
* @return 用户密码,一定是加密后的密码
*/
@Override
public String getPassword() {
return sysUser.getPassword();
}
/**
* @return 用户名
*/
@Override
public String getUsername() {
return sysUser.getUsername();
}
/**
* @return 账户是否过期
*/
@Override
public boolean isAccountNonExpired() {
return sysUser.getAccountNoExpired() != 0;
}
/**
* @return 账户是否被锁住
*/
@Override
public boolean isAccountNonLocked() {
return sysUser.getAccountNoLocked() !=0;
}
/**
* @return 凭据是否过期
*/
@Override
public boolean isCredentialsNonExpired() {
return sysUser.getCredentialsNoExpired() !=0 ;
}
/**
* @return 账户是否可用
*/
@Override
public boolean isEnabled() {
return sysUser.getEnabled() !=0 ;
}
}
这个地方我们之前是这么写的
2.3.2 实现UserDetailsService接口
java
@Slf4j
@Service
public class SecurityUserDetailsService implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Override
public 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;
}
}
我们之前是这么写的
2.3.3 安全配置类
java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 重写 configure(HttpSecurity http)方法
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()//授权http请求
.anyRequest()//任何请求
.authenticated();//需要验证
http.formLogin().permitAll(); //SpringSecurity的表单认证
}
/**
* @return 密码加密器
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
2.3.4 测试程序
2.3.4.1 控制器
java
@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";
}
}
java
@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
java
@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
java
@Mapper
public interface SysMenuDao {
List<String> queryPermissionByUserId(@Param("userId") Integer userId);
}
这个地方涉及到三张表,角色用户关联表sys_role_user、角色权限关联表sys_role_menu、权限表sys_menu
我们要实现通过用户获取对应的权限
sql
<mapper namespace="com.zhangjingqi.dto.SysMenuDao">
<select id="queryPermissionByUserId" resultType="java.lang.String">
SELECT distinct sm.code
FROM sys_role_user sru
inner join sys_role_menu srm
on sru.rid = srm.rid
inner join sys_menu sm on srm.mid = sm.id
where sru.uid = #{userId}
and sm.delete_flag = 0
</select>
</mapper>
3.2.3 SysMenuServiceImpl
java
@Service
public class SysMenuServiceImpl implements SysMenuService {
@Autowired
private SysMenuDao sysMenuDao;
@Override
public List<String> queryPermissionByUserId(Integer userId) {
return sysMenuDao.queryPermissionByUserId(userId);
}
}
3.2.4 修改 UserDetailsService
java
@Slf4j
@Service
public class SecurityUserDetailsService implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysMenuService sysMenuService;
@Override
public 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
java
@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 权限信息
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return simpleGrantedAuthorities;
}
/**
* @return 用户密码,一定是加密后的密码
*/
@Override
public String getPassword() {
return sysUser.getPassword();
}
/**
* @return 用户名
*/
@Override
public String getUsername() {
return sysUser.getUsername();
}
/**
* @return 账户是否过期
*/
@Override
public boolean isAccountNonExpired() {
return sysUser.getAccountNoExpired() != 0;
}
/**
* @return 账户是否被锁住
*/
@Override
public boolean isAccountNonLocked() {
return sysUser.getAccountNoLocked() !=0;
}
/**
* @return 凭据是否过期
*/
@Override
public boolean isCredentialsNonExpired() {
return sysUser.getCredentialsNoExpired() !=0 ;
}
/**
* @return 账户是否可用
*/
@Override
public boolean isEnabled() {
return sysUser.getEnabled() !=0 ;
}
}
3.3 测试
使用Obama登录
查看obama权限
四、总结
认证与授权,需要我们实现UserDetails用户详情类和UserDetailsService类