Day118 Spring Security
Spring Security是一个保护Java Web应用安全的框架,其核心是基于过滤器链实现请求拦截和认证授权。本文介绍了Spring Security的基本结构、内置认证功能以及如何自定义用户认证流程。主要内容包括: Spring Security采用过滤器链机制,通过SecurityFilterChain提供默认的拦截规则和登录逻辑。 内置案例演示了基本功能,包括自动生成的登录页面和默认用
Spring Security
https://gitee.com/peng-chuanbin/framework-learn.git
SpringSecurity:保护JavaWeb网站安全的框架
环境需求:
JDK1.8
SpringBoot2.7.6 版本下的 SpringSecurity版本为 5.7.5
阿里云脚手架
1.SpringSecurity结构(内部原理)
1.1 JavaWeb 内部结构
客户端(浏览器或者其他APP)发送请求,先经过多个过滤器,然后最后交给Servlet进行处理

1.2 SpringSecurity 内部结构
SpringSecurity的核心其实就是Filter
SecurityFilterChain 是SpringSecurity的默认过滤器链,这个过滤器链提供了拦截规则以及登录逻辑

2.SpringSecurity内置案例
官方内置了一套简单的验证逻辑,自带登录页面(登录功能)和注销以及内置账户和密码,方便开发者快速入门
2.1 环境准备
1.创建SpringBoot项目,添加依赖
SpringBoot版本采用的 2.7.6,Java开发环境基于JDK1.8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- SpringSecurity 测试依赖 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
2.项目创建完成后,添加测试代码 HelloController.java,运行项目
通过浏览器 http://localhost:1002/FrameworkLearn/test1或者test2,查看效果
package com.learn.frameworklearn.controller;
@RestController
public class HelloController {
//测试方法1 匿名访问
@GetMapping(value = "/test1")
public Result test1(){
return Result.success("test1");
}
//测试方法2 认证后才能访问
@GetMapping(value = "/test2")
public Result test2(){
return Result.success("test2");
}
}

用户名:user
密码:控制台

注意:如果需要放开security拦截,不走security内置登录界面
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
2.2 内置案例详细介绍
SpringSecurity内置案例核心API
InMemoryUserDetailsManager:内置账户信息管理类,是UserDetailsService的子类
UserDetailsService:SpringSecurity用户信息管理类的核心接口,管理用户信息来源(数据库还是内存以及其他…)
UserDetails:SpringSecurity封装用户信息的核心接口,给SpringSecurity送用户信息时,SpringSecurity只认UserDetails
2.3 修改内置的用户名和密码
配置文件方式
spring:
security:
user:
name: admin
password: 123
3.替换系统自带用户名和密码的获取方式
3.1 认证的运行原理

3.2 替换默认生成的用户信息
不修改前端,只修改后端,从数据库拿账号密码(从数据库中获取账号密码传递给SpringSecurity上下文)
3.2.1 数据库表设计
创建securityrabc数据库,导入sql文件
-- 当前流行的权限控制系统 RBAC 模式,所以数据库表设计基于 RBAC
-- 用户表
CREATE TABLE user(
user_id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID主键',
phone VARCHAR(50) NOT NULL UNIQUE COMMENT '手机号,唯一',
password VARCHAR(255) NOT NULL COMMENT '密码',
usernameVARCHAR(100) NOT NULL COMMENT '用户名'
)AUTO_INCREMENT=1000 DEFAULT charset=utf8 COMMENT='用户表';
-- 角色表
CREATE TABLE role(
role_id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '角色ID主键',
role_name VARCHAR(100) NOT NULL COMMENT '角色名'
)AUTO_INCREMENT=1000 DEFAULT charset=utf8 COMMENT='角色表';
-- 权限表
CREATE TABLE permission(
permission_id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '权限ID主键',
permission_name VARCHAR(100) NOT NULL COMMENT '权限名'
)AUTO_INCREMENT=1000 DEFAULT charset=utf8 COMMENT='权限表';
-- 用户角色关联表
CREATE TABLE user_role(
user_id BIGINT NOT NULL COMMENT '用户ID',
role_id BIGINT NOT NULL COMMENT '角色ID',
PRIMARY KEY (user_id,role_id)
) DEFAULT charset=utf8 COMMENT='用户角色关联表';
-- 角色权限关联表
CREATE TABLE role_permission(
role_id BIGINT NOT NULL COMMENT '角色ID',
permission_id BIGINT NOT NULL COMMENT '权限ID',
PRIMARY KEY (role_id,permission_id)
) DEFAULT charset=utf8 COMMENT='角色权限关联表';
3.2.2 添加依赖
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- ORM框架 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<!-- 省略GET/SET等工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
3.2.3 代码实现
1.配置文件(配置数据库,配置mybatis)
spring:
# 数据源配置(Druid)
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
url: jdbc:mysql://127.0.0.1:3306/securityrabc?characterEncoding=utf-8
username: root
password: 123456
# MyBatis 配置
mybatis:
# 别名包(自动为该包下类注册别名)
typeAliasesPackage: com.learn.frameworklearn.**.entity
# Mapper XML 文件位置
mapperLocations: classpath:mybatis/**/*Mapper.xml
configuration:
map-underscore-to-camel-case: true # 驼峰命名
2.映射数据库表的实体类
/**
* 数据库用户表 - user
*/
@Data
public class User {
/**
* 用户ID
*/
private Long userId;
/**
* 手机号
*/
private String phone;
/**
* 密码
*/
private String password;
/**
* 用户名
*/
private String username;
}
/**
* 数据库角色表 - role
*/
@Data
public class Role {
/**
* 角色ID
*/
private Long roleId;
/**
* 角色名称
*/
private String roleName;
}
/**
* 数据库权限表 - permission
*/
@Data
public class Permission {
/**
* 权限ID
*/
private Long permissionId;
/**
* 权限名称
*/
private String permissionName;
}
/**
* 数据库用户角色关联表 - user_role
*/
@Data
public class UserRole {
/**
* 用户ID
*/
private Long userId;
/**
* 角色ID
*/
private Long roleId;
}
/**
* 数据库角色权限关联表 - role_permission
*/
@Data
public class RolePermission {
/**
* 角色ID
*/
private Long roleId;
/**
* 权限ID
*/
private Long permissionId;
}
3.mapper层实现使用用户名获取用户信息的函数
在mapper包中新建一个UserMapper接口
package com.learn.frameworklearn.mapper;
/**
* 用户表操作,使用手机号用来代替用户登录账号
*/
@Mapper
public interface UserMapper {
/**
* 通过手机号获取用户信息
* phone 手机号
* @return 用户信息
*/
User getUserByPhone(String phone);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learn.frameworklearn.mapper.UserMapper">
<select id="getUserByPhone" resultType="com.learn.frameworklearn.entity.User">
SELECT * FROM user WHERE phone=#{phone}
</select>
</mapper>
4.UserDetails接口实现,封装用户信息(给SpringSecurity送数据)
package com.learn.frameworklearn.security;
/**
* 封装用户信息
* SpringSecurity规定给他传递的用户信息,必须是UserDetails接口的子类实例对象进行封装
*/
@Data
public class LoginUserDetails implements UserDetails {
private User user;
public LoginUserDetails() {
}
public LoginUserDetails(User user) {
this.user = user;
}
//当前账户的权限列表,暂时只实现认证,不实现授权,所以这边权限给空集合
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return new ArrayList<>();
}
//获取密码
@Override
public String getPassword() {
return user.getPassword();
}
//用户名
@Override
public String getUsername() {
return user.getPhone();
}
//账号是否过期,在数据库中没有设置,给默认值不过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//账号是否锁定,在数据库中没有设置,给默认值不锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
//密码是否过期,在数据库中没有设置,给默认值不过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//账号是否可用,在数据库中没有设置,给默认值可用
@Override
public boolean isEnabled() {
return true;
}
}
5.替换 InMemoryUserDetailsManager 实现 UserDetailsService方法
package com.learn.frameworklearn.security;
/**
* UserDetailsService是Spring Security提供的从数据库获取数据的核心接口
* 实现 UserDetailsService 重写里面的 loadUserByUsername方法,替换默认从内存中获取用户信息
*/
@Transactional
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
/**
* 根据用户名查询用户信息
* @return 不能直接返回user对象,必须返回UserDetails对象,所以需要重写一个UserDetails对象返回
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//通过用户名从数据库中查询用户信息(这个用户名从前端传递过来时可以使用手机号,邮箱或者其他用户账号)
User user = userMapper.getUserByPhone(username);
//判断当前账号是否存在
if(Objects.isNull(user)){
//如果为空,直接抛出异常
throw new UsernameNotFoundException(username);
}
/*
* 不为空说明数据库中存在,将信息送到SpringSecurity上下文中
*/
return new LoginUserDetails(user);
}
}

6.在数据库user表中添加测试账户

7.验证基于数据库的登录是否生效替换了默认用户名和密码的形式
验证是否生效
第一步: 启动项目
第二步: 在浏览器中输入 http://localhost:1002/FrameworkLearn/test1 服务会自动跳转到SpringSecurity的内置登录页面
第三步: 数据在数据库中自己添加的用户信息
第四步: 查看现象
注意: 肯定会失败,因为密码,因为密码在数据库中是明文的,需要设置明文规则,如果在密码前添加{noop},可以不需要进行加密(security默认密码是加密的)修改密码后,浏览器中继续测试,就会成功

8.如果想数据库中的密码是密文,可以使用 BCryptPasswordEncoder 进行加密和解密
创建配置类
将 BCryptPasswordEncoder 加入到IOC容器中,SpringSecurity 自动生效
package com.learn.frameworklearn.security;
/**
* Spring Security配置类
*/
@Configuration
public class SecurityConfig {
/**
* 配置加密工具
*/
@Bean
public PasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
}

编写测试代码,将明文密码加密成密文,保存到数据库中,然后在继续使用浏览器测试

替换数据库中的密码
注意:数据库中的密码不能明文存储,要加密,而且对于加密工具,只有加密没有解密的说法,不能把加密后的密码资再解密成123456,没有这种做法
参考:市面上大部分软件的忘记密码功能,都是修改密码,再重新登录,而不是真正的找回密码

4.替换页面登录为前后端分离方式
修改前端界面(封装前端认证信息传递给SpringSecurity上下文)
前后端分离架构是当前最流行的软件架构模式,下面需要修改SpringSecurity默认的逻辑,改成成前后端分离的架构模式
1.替换掉登陆页面
2.修改从内置登录页面获取用户名和密码的逻辑
3.修改内部默认跳转登录页面的逻辑
4.修改登录失败的逻辑
4.1 替换内置登录验证逻辑
4.1.1 登录验证流程图
此流程图是基于前后端不分离的表单流程图,和前后端分离的流程基本上一模一样,后面实现前后端分离的登录,参考此流程

4.1.2 登录核心API介绍
SecurityFilterChain:SpringSecurity核心过滤器,SpringSecurity默认会自动创建一个此对象,用来支持自带的登录,现在采用前后端分离,登录逻辑发生变化,所以需要我们自己创建SpringSecurity来覆盖默认的
UsernamePasswordAuthenticationToken:封装前端页面传递过来的用户名和密码,封装好后通过
AuthenticationManager传递给SpringSecurity上下文
AuthenticationManager:SpringSecurity的认证管理器,用来进行认证
Authentication:认证实例,认证成功后,里面封装认证成功后的信息
4.1.3 登录逻辑代码实现
配置认证管理器实例
package com.learn.frameworklearn.security;
@Configuration
public class SecurityConfig {
/**
* SpringSecurity 认证管理器
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
配置 SecurityFilterChain
package com.learn.frameworklearn.security;
/**
* Spring Security配置类
*/
@Configuration
public class SecurityConfig {
/**
* 配置加密工具
*/
@Bean
public PasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
/**
* SpringSecurity 认证管理器
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
/**
* SpringSecurity过滤器
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable() //防止跨站请求伪造
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //取消session
.and()
.authorizeRequests()
.antMatchers("/login").permitAll() //这个/login地址,登陆和未登录的人都可以访问访问
.anyRequest().authenticated(); //除了上面设置的地址可以匿名访问,其它所有的请求地址需要认证访问
return http.build();
}
}
登录逻辑实现
@RestController
public class LoginController {
@Resource
private AuthenticationManager authenticationManager;
/**
* 登录
* phone 手机号
* password 密码
* @return 响应结果
*/
@PostMapping(value = "/login")
public Result login(String phone, String password){
/*
* SpringSecurity 的认证逻辑
* 默认情况SpringSecurity内置了登录页面,内置了从页面获取数据,并将其数据送到SpringSecurity上下文的方式
* 当前前后端分离的逻辑,数据不再从页面获取,所以不能再使用内置逻辑,需要程序员自己实现将数据送到SpringSecurity上下文中
*/
//封装用户名(手机号作为用户名)和密码
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(phone,password);
//调用认证管理中的认证方法,成功就封装用户的全部信息,但是调用后可能出现异常,所以需要try...catch
try {
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//如果认证成功Authentication中就会有用户信息,否则为空
if(Objects.isNull(authenticate)){
//认证失败
return Result.error(301,"认证失败,用户名或密码错误");
}
}catch (RuntimeException e){
e.printStackTrace();
//认证失败
return Result.error(302,"认证失败,用户名或密码错误");
}
return Result.success();
}
}
代码完成后进行测试,因为没有前端页面所以需要一个客户端工具进行测试,这里采用postman进行测试
注意:.antMatchers(“/login”).permitAll() //这个/login地址,登陆和未登录的人都可以访问访问


还有另外一种情况,在登录了的情况下,访问了其它资源,例如: http://localhost:1002/FrameworkLearn/test2
注意: 我前面已经登陆了为什么还不能访问,因为前后端分离项目不是Session-Cookie机制在前后端分离的项目中,传统的Session-Cookie机制可能不再适用,因为前端和后端是独立运行的。在这种情况下,通常会使用其他认证和授权机制,如JWT(JSON Web Tokens)或OAuth2等

先写一个不携带token访问其他资源的逻辑处理
4.2 修改未登录访问其它资源的逻辑处理
实现此功能需要的步骤如下:
第二步: 实现AuthenticationEntryPoint接口,实现自定义的处理器
第三步: 将自定义的处理器注册到Spring Security的核心Filter中
第四步: 测试是否生效
第一步: 添加依赖(json工具依赖)
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.32</version>
</dependency>
第二步
package com.learn.frameworklearn.security;
/**
* 匿名请求访问私有化请求时的处理器(直接告诉用户未认证)
* 在未认证或者认证错误的情况下访问需要认证的资源时的处理类
*/
@Component //加入到IOC容器
public class LoginUnAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
/*
* 当访问一个需要认证的资源时因为当前用户没有认证或者认证失败,直接访问资源会交给此函数进行处理
* 因为架构是前后端分离的项目,所以给客户端的提示保持和控制器的返回值格式相同
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
Result result = Result.error(303, "用户未认证或登录已过期,请重新登录后再访问");
//将消息json化
String json = JSONUtil.toJsonStr(result);
//送到客户端
response.getWriter().print(json);
}
}
第三步
将自定义的处理器注册到Spring Security的核心过滤器中
package com.learn.frameworklearn.security;
@Configuration
public class SecurityConfig {
//注入进来
@Resource
private LoginUnAuthenticationEntryPointHandler loginUnAuthenticationEntryPointHandler;
/**
* SpringSecurity过滤器
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable() //防止跨站请求伪造
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //取消session
.and()
.authorizeRequests()
.antMatchers("/login").permitAll() //这个/login地址,登陆和未登录的人都可以访问访问
.anyRequest().authenticated(); //除了上面设置的地址可以匿名访问,其它所有的请求地址需要认证访问
//自定义未登录处理(注册匿名请求访问私有化请求时的处理器) 添加这个
http.exceptionHandling().authenticationEntryPoint(loginUnAuthenticationEntryPointHandler);
return http.build();
}
}
第四步:测试

4.3 解决HTTP协议无状态
传统前后端不分离的架构,采用Session+Cookie机制(解决HTTP协议无状态)
现在前后端分离架构,采用token令牌方式
token生成采用JWT工具生成和校,使用Redis数据库进行校验和保存
4.3.1 Redis客户端安装和启动
省略
3.2 JWT工具类编写
添加依赖
<!-- JWT工具类 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<!-- redis客户端 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
工具类代码实现
package com.learn.frameworklearn.utils;
/**
* JWT工具类
*/
public class JwtUtils {
private static Algorithm hmac256 = Algorithm.HMAC256("YLWTSMTJFYHDCMGSCWHSSYBZSDKC");
/**
* 生成token
* @param pub 负载
* @param expiresTime 过期时间(单位 毫秒)
* @return token
*/
public static String sign(String pub, Long expiresTime){
return JWT.create() //生成令牌函数
.withIssuer(pub) //自定义负载部分,其实就是添加Claim(jwt结构中的payload部分),可以通过源码查看
.withExpiresAt(new Date(System.currentTimeMillis()+expiresTime)) //添加过期时间
.sign(hmac256);
}
/**
* 校验token
*/
public static boolean verify(String token){
JWTVerifier verifier = JWT.require(hmac256).build();
//如果正确,直接代码向下执行,如果错误,抛异常
verifier.verify(token);
return true;
}
/**
* 从token中获取负载
* @param token 令牌
* @return 保存的负载
*/
public static String getClaim(String token){
DecodedJWT jwt = JWT.decode(token);
Claim iss = jwt.getClaim("iss");
return iss.asString();
}
}
4.3.3 Redis客户端实现
配置文件修改
spring:
redis:
host: 127.0.0.1 # redis服务器地址
password: # redis服务器密码,我这里没有设置密码
database: 0 # redis的库,我这里用0号库
代码实现
package com.learn.frameworklearn.utils;
/**
* Redis客户端
*/
@Component
public class RedisClient {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 保存数据
*/
public void set (String key,String value){
stringRedisTemplate.opsForValue().set(key,value);
}
/**
* 保存数据-过期时间
* @param key 键
* @param value 值
* @param time 过期时间,单位是 毫秒
*/
public void set (String key,String value,Long time){
stringRedisTemplate.opsForValue().set(key,value,time, TimeUnit.MILLISECONDS);
}
/**
* 通过键获取对应的值
* @param key 键
* @return 值
*/
public String get(String key){
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 通过键删除对应的值
* @param key 键
*/
public void del(String key){
stringRedisTemplate.delete(key);
}
/**
* 判断key是否存在
*/
public Boolean exists(String key){
return stringRedisTemplate.hasKey(key);
}
}
4.3.4 登录逻辑修改
package com.learn.frameworklearn.controller;
@RestController
public class LoginController {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private RedisClient redisClient;
/**
* 登录
* phone 手机号
* password 密码
*
* @return 响应结果
*/
@PostMapping(value = "/login")
public Result login(String phone, String password) {
/*
* SpringSecurity 的认证逻辑
* 默认情况SpringSecurity内置了登录页面,内置了从页面获取数据,并将其数据送到SpringSecurity上下文的方式
* 当前前后端分离的逻辑,数据不再从页面获取,所以不能再使用内置逻辑,需要程序员自己实现将数据送到SpringSecurity上下文中
*/
//1.封装用户名(手机号作为用户名)和密码
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(phone, password);
//2.调用认证管理中的认证方法,成功就封装用户的全部信息,但是调用后可能出现异常,所以需要try...catch
try {
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//如果认证成功Authentication中就会有用户信息,否则为空
if (Objects.isNull(authenticate)) {
//认证失败
return Result.error(301, "认证失败,用户名或密码错误");
}
//3.登录成功将用户信息保存到redis中,以token作为key
//获取用户信息
LoginUserDetails principal = (LoginUserDetails) authenticate.getPrincipal();
if (Objects.isNull(principal)) {
return Result.error(303,"认证失败,用户名或密码错误");
}
//使用token作为redis的key,格式为 login:token
String token = JwtUtils.sign(principal.getUsername(), 1000 * 60 * 60 * 24 * 7L);//过期时间为7天
String key = "login:token:" + token;
//将用户信息json化
String json = JSONUtil.toJsonStr(principal);
//将用户信息json化后保存到redis中
redisClient.set(key, json, 1000 * 60 * 60 * 24 * 7L);
//4.将token返回给客户端(前端)
Map<String, Object> map = new HashMap<>();
map.put("token", token);
return Result.success(map);
} catch (RuntimeException e) {
e.printStackTrace();
//认证失败
return Result.error(302, "认证失败,用户名或密码错误");
}
}
}
测试,查看是否生成token,并且会把这个token存储在redis中

redis客户端工具查看

注意:之后的每次请求,都要带着token一起发送请求
4.3.5 反复登录,多token解决
客户端发送的所有请求都需要带token,在进行登录时单独进行token的校验,如果登陆过,刷新token
在登录中添加一个校验逻辑,删除原来的key
package com.learn.frameworklearn.controller;
@RestController
public class LoginController {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private RedisClient redisClient;
/**
* 登录
*/
@PostMapping(value = "/login")
public Result login(String phone, String password, HttpServletRequest request) {
//添加这个方法
/*
* 登录前判断是否上次的登录未过期,如果未过期直接删除,重新登录生成新token
* 就是刷新token,登录一次刷新一次
*/
String token_ = request.getHeader("token");
//判断token是否存在
if (StringUtils.hasText(token_)) {
//判断redis中是否存在
String claim = JwtUtils.getClaim(token_);
//校验是否是同一个账户
if (StringUtils.hasText(claim) && claim.equals(phone)) {
String key = "login:token:" + token_;
//从redis中删除
redisClient.del(key);
}
}
//......
}
}
4.3.6 其它资源访问基于token令牌方式
使用令牌方式,替换前后端不分离的session-cookie机制,解决HTTP无状态的问题
SpringSecurity运行原理
pringSecurity的功能实现:SpringSecurity的核心是过滤器链,核心功能实现也是由一个个Filter(过滤器)组成
下面是SpringSecurity内置的过滤器

会经过一系列的过滤器链
简单画了一个SpringSecurity原理图,客戶端发送过来的请求会经过一个个的过滤器,每一个过滤器承担着不同的功能
例如,UsernamePasswordAuthenticationFilter过滤器帮助我们校验账户和密码现在我们要模拟session+cookie机制,通过token凭据实现权限控制,方式如下

4.3.7 基于token机制的实现
第一步: 自定义 Filter
第二步: 注册到SpringSecurity的Filter中
第一步
package com.learn.frameworklearn.security;
/**
* 自定义过滤器,实现token令牌的判断(统一校验token凭证)
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private RedisClient redisClient;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取请求头token
String token = request.getHeader("token");
if(StringUtils.hasLength(token)){
//redis中获取用户信息
String key = "login:token:"+token;
String json = redisClient.get(key);
if(StringUtils.hasLength(json)){
//反序列化
LoginUserDetails user = JSONUtil.toBean(json, LoginUserDetails.class);
if(Objects.nonNull(user)){
//封装用户信息,送到下一个过滤器 UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
//将Redis数据库中的信息送到SpringSecurity上下文中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}else {
SecurityContextHolder.getContext().setAuthentication(null);
}
}
}
//放行,后面交给Spring Security 框架
filterChain.doFilter(request,response);
}
}
第二步
package com.learn.frameworklearn.security;
@Configuration
public class SecurityConfig {
@Resource
private LoginUnAuthenticationEntryPointHandler loginUnAuthenticationEntryPointHandler;
//注入进来
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* SpringSecurity过滤器
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable() //防止跨站请求伪造
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //取消session
.and()
.authorizeRequests()
.antMatchers("/login").permitAll() //这个/login地址,登陆和未登录的人都可以访问访问
.anyRequest().authenticated(); //除了上面设置的地址可以匿名访问,其它所有的请求地址需要认证访问
//将自定义的过滤器注册到SpringSecurity过滤器链中,并且设置到UsernamePasswordAuthenticationFilter前面 添加这句话
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//自定义未登录处理(注册匿名请求访问私有化请求时的处理器)
http.exceptionHandling().authenticationEntryPoint(loginUnAuthenticationEntryPointHandler);
return http.build();
}
}
测试验证


5.注销
注销比较简单,直接将redis数据清空了即可
SpringSecurity内置注销功能logout,我们使用内置的注销,覆盖注销的逻辑即可
package com.learn.frameworklearn.security;
/**
* 注销成功的处理器
*/
@Component
public class LogoutStatusSuccessHandler implements LogoutSuccessHandler {
@Resource
private RedisClient redisClient;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String token = request.getHeader("token");
if(StringUtils.hasText(token)){
redisClient.del("login:token:"+token);
}
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
Result result = Result.success("注销成功");
//将消息json化
String json = JSONUtil.toJsonStr(result);
//送到客户端
response.getWriter().print(json);
}
}
在security中注册一下注销成功的处理器
package com.learn.frameworklearn.security;
@Configuration
public class SecurityConfig {
@Resource
private LoginUnAuthenticationEntryPointHandler loginUnAuthenticationEntryPointHandler;
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Resource
private LogoutStatusSuccessHandler logoutStatusSuccessHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable() //防止跨站请求伪造
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //取消session
.and()
.authorizeRequests()
.antMatchers("/login").permitAll() //这个/login地址,登陆和未登录的人都可以访问访问
.anyRequest().authenticated(); //除了上面设置的地址可以匿名访问,其它所有的请求地址需要认证访问
//将自定义的过滤器注册到SpringSecurity过滤器链中,并且设置到UsernamePasswordAuthenticationFilter前面
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//自定义未登录处理(注册匿名请求访问私有化请求时的处理器)
http.exceptionHandling().authenticationEntryPoint(loginUnAuthenticationEntryPointHandler);
//注册自定义处理器(注销处理器,注销成功后删除redis中的数据) 添加这句话
http.logout().logoutSuccessHandler(logoutStatusSuccessHandler);
return http.build();
}
}
http://localhost:1002/FrameworkLearn/logout,使用security内置的即可,改一下逻辑

6.授权
授权的意义就是当一个用户登录成功后,此账户本身拥有的权限
eg:当前用户用哪些角色,当前角色都能干什么(删除、更新等)

6.1 将权限从数据库送到SpringSecurity上下文
从数据库中将用户信息送到上下文,在 UserDetailsService接口的实现类中
实现步骤:
第一步:mapper提供查询方法,将用户相关的权限信息查询到封装到UserDetails的对象中
通过用户ID查询角色名称列表
通过角色ID查询权限名称列表第二步:在UserDetailsService中进行封装
在UserDetailsService中调用mapper并且封装传递给 SpringSecurity 上下文,修改UserDetails结构添加属性等第三步:在控制器层进行注解控制(配置文件)
第四步:开启注解配置否则不生效
第五步:权限不够的处理器
6.1.1 第一步
/**
* 用户角色关联表
*/
@Mapper
public interface UserRoleMapper {
/**
* 通过用户ID查询用户角色列表
* @param userId 用户ID
* @return 用户角色列表
*/
List<UserRole> getUserRolesByUserId(Long userId);
}
/**
* 操作数据库角色表
*/
@Mapper
public interface RoleMapper {
/**
* 批量查询
* @param roleIds 角色ID列表
* @return 角色列表
*/
List<Role> batchGetRolesByRoleIds(List<Long> roleIds);
}
/**
* 操作数据库角色权限表
*/
@Mapper
public interface RolePermissionMapper {
/**
* 通过角色ID列表查询权限角色权限列表
* @param roleIds 角色IDs
* @return 角色权限列表
*/
List<RolePermission> getRolePermissionsByRoleIds(List<Long> roleIds);
}
/**
* 操作数据库权限表
*/
@Mapper
public interface PermissionMapper {
/**
* 通过权限ID列表查询权限列表
* @param permissionIds 权限ID列表
* @return 权限列表
*/
List<Permission> batchGetPermissionsByPermissionIds(List<Long> permissionIds);
}
mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learn.frameworklearn.mapper.PermissionMapper">
<select id="batchGetPermissionsByPermissionIds" resultType="com.learn.frameworklearn.entity.Permission">
SELECT * FROM permission WHERE permission_id IN
<foreach collection="list" open="(" close=")" separator="," item="item">
#{item}
</foreach>
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learn.frameworklearn.mapper.RolePermissionMapper">
<select id="batchGetRolePermissionsByRoleIds" resultType="com.learn.frameworklearn.entity.RolePermission">
SELECT * FROM role_permission WHERE role_id IN
<foreach collection="list" open="(" close=")" separator="," item="item">
#{item}
</foreach>
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learn.frameworklearn.mapper.RoleMapper">
<select id="batchGetRolesByRoleIds" resultType="com.learn.frameworklearn.entity.Role">
SELECT * FROM role WHERE role_id IN
<foreach collection="list" open="(" close=")" separator="," item="item">
#{item}
</foreach>
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learn.frameworklearn.mapper.UserRoleMapper">
<select id="getUserRoleByUserId" resultType="com.learn.frameworklearn.entity.UserRole">
SELECT * FROM user_role WHERE user_id=#{userId}
</select>
</mapper>

6.1.2 第二步
UserDetails 修改
package com.learn.frameworklearn.security;
/**
* 封装用户信息
* SpringSecurity规定给他传递的用户信息,必须是UserDetails接口的子类实例对象进行封装
*/
@Data
public class LoginUserDetails implements UserDetails {
private User user;
//角色名称列表,用于授权
private List<String> roleNames;
//权限名称列表,用户授权
private List<String> permissionNames;
public LoginUserDetails() {
}
public LoginUserDetails(User user, List<String> roleNames, List<String> permissionNames) {
this.user = user;
this.roleNames = roleNames;
this.permissionNames = permissionNames;
}
//当前账户的权限列表 现在写这里
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
/*
* 当前用户的权限信息
* 1. 角色表权限 ROLE_ admin ROLE_admin
* 2. 权限表权限 del add query edit
*/
//添加角色
if(!CollectionUtils.isEmpty(roleNames)){
//将角色设置到GrantedAuthority中,官网要求角色要加上前缀 ROLE_xxx 区分其它权限
for (String roleName : roleNames) {
roleName = "ROLE_"+roleName;
grantedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
}
//添加权限
if(!CollectionUtils.isEmpty(permissionNames)){
//将权限设置到GrantedAuthority中
for (String permissionName : permissionNames) {
grantedAuthorities.add(new SimpleGrantedAuthority(permissionName));
}
}
return grantedAuthorities;
}
//获取密码
@Override
public String getPassword() {
return user.getPassword();
}
//......
}
UserDetailsService修改
package com.learn.frameworklearn.security;
/**
* UserDetailsService是Spring Security提供的从数据库获取数据的核心接口
* 实现 UserDetailsService 重写里面的 loadUserByUsername方法,替换默认从内存中获取用户信息
*/
@Transactional
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Resource
private UserRoleMapper userRoleMapper;
@Resource
private RoleMapper roleMapper;
@Resource
private RolePermissionMapper rolePermissionMapper;
@Resource
private PermissionMapper permissionMapper;
/**
* 根据用户名查询用户信息
*
* @param username
* @return 不能直接返回user对象,必须返回UserDetails对象,所以需要重写一个UserDetails对象返回
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//通过用户名从数据库中查询用户信息(这个用户名从前端传递过来时可以使用手机号,邮箱或者其他用户账号)
User user = userMapper.getUserByPhone(username);
//判断当前账号是否存在
if (Objects.isNull(user)) {
//如果为空,直接抛出异常
throw new UsernameNotFoundException(username);
}
//添加这里
/*
* 不为空说明数据库中存在,将信息送到SpringSecurity上下文中(用户信息,角色信息,权限信息都要返回给客户端)
*/
//角色名称列表
List<String> roleNames = new ArrayList<>();
//权限名称列表
List<String> permissionNames = new ArrayList<>();
//获取用户角色列表
List<UserRole> userRoles = userRoleMapper.getUserRoleByUserId(user.getUserId());
if (!CollectionUtils.isEmpty(userRoles)) {
//查询角色信息
List<Long> roleIds = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(roleIds)) {
//查询角色信息
List<Role> roles = roleMapper.batchGetRolesByRoleIds(roleIds);
if (!CollectionUtils.isEmpty(roles)) {
List<String> roleNameList = roles.stream().map(Role::getRoleName).collect(Collectors.toList());
roleNames.addAll(roleNameList);
}
//当前角色的权限列表
List<RolePermission> rolePermissions = rolePermissionMapper.batchGetRolePermissionsByRoleIds(roleIds);
if (!CollectionUtils.isEmpty(rolePermissions)) {
//权限id列表
List<Long> permissionIdList = rolePermissions.stream().map(RolePermission::getPermissionId).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(permissionIdList)) {
//查询权限信息
List<Permission> permissions = permissionMapper.batchGetPermissionsByPermissionIds(permissionIdList);
if (!CollectionUtils.isEmpty(permissions)) {
List<String> permissionNameList = permissions.stream().map(Permission::getPermissionName).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(permissionNameList)) {
//权限名称列表
permissionNames.addAll(permissionNameList);
}
}
}
}
}
}
//用户信息,角色信息,权限信息都要返回给客户端
return new LoginUserDetails(user, roleNames, permissionNames);
}
}
6.1.3 第三步
在配置文件上添加注解,开启注解校验
/**
* 测试控制器
* 验证SpringSecurity是否生效
*/
@RestController
public class HelloController {
//测试方法1 匿名访问(pom.xml中注释掉security的依赖就可以随便访问,添加了依赖,访问网址就会默认跳转到内置网页)
@GetMapping(value = "/test1")
public Result test1(){
return Result.success("test1");
}
//测试方法2 认证后才能访问
@GetMapping(value = "/test2")
public Result test2(){
return Result.success("test2");
}
//测试方法3 admin角色可以访问
@PreAuthorize(value = "hasRole('admin')") //开启@EnableMethodSecurity(securedEnabled = true)测试
@GetMapping(value = "/test3")
public Result test3(){
return Result.success("test3");
}
//测试方法4 cto角色或者cfo角色可以访问
@PreAuthorize(value = "hasAnyRole('cfo','cto')")
@GetMapping(value = "/test4")
public Result test4(){
return Result.success("test4");
}
//测试方法5 cto角色和cfo角色可以访问
@PreAuthorize(value = "hasRole('cto') and hasRole('admin')")
@GetMapping(value = "/test5")
public Result test5(){
return Result.success("test5");
}
//测试方法6 del权限可以访问
@PreAuthorize(value = "hasAuthority('del')")
@GetMapping(value = "/test6")
public Result test6(){
return Result.success("test6");
}
//测试方法7 del或者edit权限可以访问
@PreAuthorize(value = "hasAnyAuthority('del','edit')")
@GetMapping(value = "/test7")
public Result test7(){
return Result.success("test7");
}
//测试方法8 del和edit权限可以访问
@PreAuthorize(value = "hasAuthority('del') and hasAuthority('edit')")
@GetMapping(value = "/test8")
public Result test8(){
return Result.success("test8");
}
}
6.1.4 第四步
如果登录成功,使用当前用户进行访问;如果登录失败,发现权限不够,报权限错误,定义处理器进行处理
package com.learn.frameworklearn.security;
/**
* 权限不足处理器
* 用户登录成功,访问某一个资源时因为权限不足,报异常
*/
@Component
public class LoginUnAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
Result result = Result.error(305,"权限不足,请重新授权。");
//将消息json化
String json = JSONUtil.toJsonStr(result);
//送到客户端
response.getWriter().print(json);
}
}
6.1.5 第五步
自定义权限不足处理器后,需要进行注册,注册到SecurityConfig配置文件中
package com.learn.frameworklearn.security;
@Configuration
@EnableMethodSecurity(securedEnabled = true) //开发方法权限验证
public class SecurityConfig {
@Resource
private LoginUnAuthenticationEntryPointHandler loginUnAuthenticationEntryPointHandler;
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Resource
private LogoutStatusSuccessHandler logoutStatusSuccessHandler;
@Resource
private LoginUnAccessDeniedHandler loginUnAccessDeniedHandler;
/**
* SpringSecurity过滤器
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable() //防止跨站请求伪造
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //取消session
.and()
.authorizeRequests()
.antMatchers("/login","/test1").permitAll() //这个/login地址,登陆和未登录的人都可以访问访问
.anyRequest().authenticated(); //除了上面设置的地址可以匿名访问,其它所有的请求地址需要认证访问
//将自定义的过滤器注册到SpringSecurity过滤器链中,并且设置到UsernamePasswordAuthenticationFilter前面
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//自定义未登录处理(注册匿名请求访问私有化请求时的处理器)
http.exceptionHandling().authenticationEntryPoint(loginUnAuthenticationEntryPointHandler);
//注册自定义的处理器(认证后的用户访问需要认证资源时因为权限不足走的处理器) 添加这里
http.exceptionHandling().accessDeniedHandler(loginUnAccessDeniedHandler);
//注册自定义处理器(注销处理器,注销成功后删除redis中的数据)
http.logout().logoutSuccessHandler(logoutStatusSuccessHandler);
return http.build();
}
}
http://localhost:1002/FrameworkLearn/login 先登录,拿到token

携带token,访问http://localhost:1002/FrameworkLearn/test2

携带token,访问http://localhost:1002/FrameworkLearn/test3

更多推荐



所有评论(0)