Spring Security
项目Demo的代码仓库:https://gitee.com/cluski/spring-security-demo.git
参考B站视频:https://www.bilibili.com/video/BV1mm4y1X7Hc
0 简介
Spring Security是Spring家族中的一个安全管理框架。相比于另一个安全框架Shiro,他提供了更丰富的功能,社区资源也比Shiro丰富。
一般来说中大型的项目都是使用Spring Security来做安全框架。小项目Shiro的比较多,因为相比与Spring Security,Shiro的上手更加的简单。
一般Web应用的需要进行认证和授权。
- 认证:验证房钱访问系统的是不是本系统的用户,并且要确认具体是哪个用户
- 授权:经过认证后判断当前用户是不是有权限进行某个操作
而认证和授权也是Spring Security作为安全框架的核心功能。
1 快速入门
1.1 准备工作
我们先要搭一个简单的SpringBoot工程
① 设置pom.xml,添加Spring Security的依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
② 创建Controller
package com.example.securitydemo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "hello";
}
}
2.2 启动应用并访问
访问/hello的端点之后,会自动跳到Spring Security的登录页面。

直接通过Postman等工具请求的话,也会返回401

默认的用户名是user,而密码(如果没有配置的话)打印在控制台当中。

在浏览器中输入用户名和密码之后,就可以直接登录了。

2 认证
2.1 登录校验流程

2.2 原理初探
想要知道如何实现自己的登录流程就必须要先知道入门案例中的SpringSecurity的流程。
2.2.1 SpringSecurity完整流程
SpringSecurity的原理其实就是过滤器链,内部包含了各种功能的过滤器。这里我们可以看看入门案例中的过滤器。

UserNamePasswordAuthenticationFilter:负责处理我们在登录页面填写了用户名和密码后登录请求。入门案例的认证保证工作主要由他负责。
ExceptionTransactionFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。
FilterSecurityInterceptor:负责校验权限的拦截器。
我们可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及他们的顺序。

2.2.2 认证流程详解

概念速查:
Authentication接口:它的实现类,表示当前访问系统的用户,封装了用户的相关信息。
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetail接口:提供核心用户信息。通过UserDetailService根据用户名处理的用户信息要封装成UserDetail对象返回。然后将这些封装到Authentication对象中。
2.3 解决问题
2.3.1 思路分析
登录:
- 自定义登录接口,调用ProviderManager的方法进行认证。如果通过,①生成JWT,②将用户信息存入Redis
- 自定义UserDetailService,在这个实现类中查询数据库
校验:
- 定义JWT认证过滤器
- 获取Token
- 解析Token,获取其中的UserId,并从Redis中取出用户信息
- 存入SecurityContextHolder中

如果认证通过,办法JWT之后,可以将用户的信息存入Redis(key:UserId、value:用户信息)。在下图的Jwt认证过滤器中可以从Redis中取出对应用户的信息。

2.3.2 准备工作
① 添加依赖
<!-- Redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redisson依赖 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.8</version>
</dependency>
<!-- fastjson依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.79</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!-- mybatis plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- druid连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
② 添加Redis相关配置
自定义序列化类
package top.cluski.tokendemo.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
static {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
private final Class<T> clazz;
public FastJsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
/**
* 序列化
*/
@Override
public byte[] serialize(T t) throws SerializationException {
if (null == t) {
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
/**
* 反序列化
*/
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (null == bytes || bytes.length <= 0) {
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return (T) JSON.parseObject(str, clazz);
}
}
配置RedisTemplate
package top.cluski.tokendemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import top.cluski.tokendemo.utils.FastJsonRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用fastJson
template.setValueSerializer(fastJsonRedisSerializer);
// hash的value序列化方式采用fastJson
template.setHashValueSerializer(fastJsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
Redis通用工具
package top.cluski.tokendemo.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* spring redis 工具类
**/
@Component
public class RedisUtil {
@Resource
private RedisTemplate<Object, Object> redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @return 缓存的对象
*/
public ValueOperations<Object, Object> setCacheObject(Object key, Object value) {
ValueOperations<Object, Object> operation = redisTemplate.opsForValue();
operation.set(key, value);
return operation;
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
* @return 缓存的对象
*/
public ValueOperations<Object, Object> setCacheObject(Object key, Object value, Integer timeout, TimeUnit timeUnit) {
ValueOperations<Object, Object> operation = redisTemplate.opsForValue();
operation.set(key, value, timeout, timeUnit);
return operation;
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public Object getCacheObject(Object key) {
ValueOperations<Object, Object> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public void deleteObject(Object key) {
redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection
*/
public void deleteObject(Collection collection) {
redisTemplate.delete(collection);
}
public Long getExpire(String key) {
return redisTemplate.getExpire(key);
}
public void expire(String key, int expire, TimeUnit timeUnit) {
redisTemplate.expire(key, expire, timeUnit);
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public ListOperations<Object, Object> setCacheList(Object key, List<Object> dataList) {
ListOperations listOperation = redisTemplate.opsForList();
if (null != dataList) {
int size = dataList.size();
for (Object o : dataList) {
listOperation.leftPush(key, o);
}
}
return listOperation;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public List<Object> getCacheList(String key) {
List<Object> dataList = new ArrayList<>();
ListOperations<Object, Object> listOperation = redisTemplate.opsForList();
Long size = listOperation.size(key);
if (null != size) {
for (int i = 0; i < size; i++) {
dataList.add(listOperation.index(key, i));
}
}
return dataList;
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public BoundSetOperations<Object, Object> setCacheSet(String key, Set<Object> dataSet) {
BoundSetOperations<Object, Object> setOperation = redisTemplate.boundSetOps(key);
for (Object o : dataSet) {
setOperation.add(o);
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public Set<Object> getCacheSet(Object key) {
Set<Object> dataSet = new HashSet<>();
BoundSetOperations<Object, Object> operation = redisTemplate.boundSetOps(key);
dataSet = operation.members();
return dataSet;
}
/**
* 缓存Map
*
* @param key
* @param dataMap
* @return
*/
public HashOperations<Object, Object, Object> setCacheMap(Object key, Map<Object, Object> dataMap) {
HashOperations hashOperations = redisTemplate.opsForHash();
if (null != dataMap) {
for (Map.Entry<Object, Object> entry : dataMap.entrySet()) {
hashOperations.put(key, entry.getKey(), entry.getValue());
}
}
return hashOperations;
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public Map<Object, Object> getCacheMap(Object key) {
Map<Object, Object> map = redisTemplate.opsForHash().entries(key);
return map;
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<Object> keys(String pattern) {
return redisTemplate.keys(pattern);
}
}
③ 响应类
package top.cluski.tokendemo.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResponseResult<T> {
private Integer code;
private String msg;
private T data;
}
④ 工具类
package top.cluski.tokendemo.utils;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class WebUtil {
public static String renderString(HttpServletResponse response, String string) {
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
⑤ 实体类
package top.cluski.tokendemo.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user")
public class User implements Serializable {
private static final long serialVersionUID = 2236272951447885407L;
@TableId
private Long id;
private String userName;
private String nickName;
private String password;
private String status;
private String email;
private String phoneNumber;
private String sex;
private String avatar;
private String userType;
private Long createBy;
private Date createTime;
private Long updateBy;
private Date updateTime;
private Integer delFlag;
}
2.3.3 实现
2.3.3.1 数据库用户校验
从之前分析我们可以知道,我肯可以定义一个UserDetailsService,让Spring Security使用我们的UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码。
准备工作
我们先创建一个用户表,建表语句如下:
create table sys_user
(
id bigint(20) not null auto_increment comment '主键',
user_name varchar(64) not null comment '登录账号',
nick_name varchar(64) not null comment '用户昵称',
password varchar(64) not null comment '密码',
status char(1) default '0' comment '帐号状态(0正常 1停用)',
email varchar(50) default '' comment '用户邮箱',
phone_number varchar(32) default '' comment '手机号码',
sex char(1) default '0' comment '用户性别(0男 1女 2未知)',
avatar varchar(128) default '' comment '头像',
user_type char(1) not null default '1' comment '用户类型(0管理员 01普通用户)',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
del_flag char(1) default '0' comment '删除标志(0代表存在 1代表删除)',
primary key (id)
) engine = innodb
auto_increment = 100 comment = '用户信息表';
引入mysql驱动依赖和mybatis-plus依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
配置数据库信息
spring:
datasource:
druid:
url: jdbc:mysql://192.168.124.29/test_db?characterEncoding=utf-8&serverTimeZone=Asia/Shanghai
username: root
password: root
修改User实体类
类型上加@TableName("sys_user"),id字段上加上@TableId
配置Mapper扫描
@SpringBootApplication
@MapperScan("top.cluski.tokendemo.mapper")
public class TokenDemoApplication {
public static void main(String[] args) {
SpringApplication.run(TokenDemoApplication.class, args);
}
}
测试Mapper是否正常
package top.cluski;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import top.cluski.tokendemo.TokenDemoApplication;
import top.cluski.tokendemo.domain.User;
import top.cluski.tokendemo.mapper.UserMapper;
import javax.annotation.Resource;
import java.util.List;
@SpringBootTest(classes = TokenDemoApplication.class)
public class MapperTest {
@Resource
private UserMapper userMapper;
@Test
public void testUserMapper() {
List<User> users = userMapper.selectList(null);
System.out.println(users);
}
}
核心代码实现
创建一个类实现UserDetailsService接口,重写其中的方法。增加用户名从数据库中查询用户信息。
package top.cluski.tokendemo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import top.cluski.tokendemo.domain.LoginUser;
import top.cluski.tokendemo.domain.User;
import top.cluski.tokendemo.mapper.UserMapper;
import javax.annotation.Resource;
import java.util.Objects;
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName, username);
User user = userMapper.selectOne(queryWrapper);
// 如果没有查询到用户,抛出异常
if (Objects.isNull(user)) {
throw new UsernameNotFoundException("用户名或者密码错误");
}
// TODO: 查询对应的权限
return new LoginUser(user);
}
}
因为UserDetailService方法的返回值是UserDetails类型,所以需要定义一个类,实现该接口,把用户信息封装在其中。
package top.cluski.tokendemo.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
2.3.3.2 密码加密储存
实际项目中我们不会把密码明文存储在数据库中。
默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。
我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。
package top.cluski.tokendemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.3.3.3 登录接口
接下我们需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。
在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。
① 定义Controller
package top.cluski.tokendemo.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import top.cluski.tokendemo.domain.ResponseResult;
import top.cluski.tokendemo.domain.User;
import top.cluski.tokendemo.service.LoginService;
import javax.annotation.Resource;
@RestController
public class LoginController {
@Resource
private LoginService loginService;
@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user) {
return loginService.login(user);
}
}
② 将AuthenticationManager注册到Spring容器中
package top.cluski.tokendemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 将登录接口放行
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 关闭csrf
.csrf().disable()
// 不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口,允许匿名访问
.antMatchers("/user/login").anonymous()
// 除上面的接口所有请求全部需要鉴权认证
.anyRequest().authenticated();
}
/**
* 将AuthenticationManager注册到IOC中
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
③ Service接口
package top.cluski.tokendemo.service;
import top.cluski.tokendemo.domain.ResponseResult;
import top.cluski.tokendemo.domain.User;
public interface LoginService {
ResponseResult login(User user);
}
ServiceImpl
package top.cluski.tokendemo.service.impl;
import cn.hutool.jwt.JWTUtil;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import top.cluski.tokendemo.domain.LoginUser;
import top.cluski.tokendemo.domain.ResponseResult;
import top.cluski.tokendemo.domain.User;
import top.cluski.tokendemo.service.LoginService;
import top.cluski.tokendemo.utils.RedisUtil;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Service
public class LoginServiceImpl implements LoginService {
@Resource
private AuthenticationManager authenticationManager;
@Resource
RedisUtil redisUtil;
@Override
public ResponseResult login(User user) {
// AuthenticationManager authenticate 进行用户认证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 如果认证没有通过,给出对应的提示
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登录失败");
}
// 如果认证通过,使用userId生成一个jwt,jwt存入ResponseResult返回
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
Long userId = loginUser.getUser().getId();
Map<String, Object> map = new HashMap<String, Object>() {
private static final long serialVersionUID = 1L;
{
put("uid", userId);
put("expire_time", System.currentTimeMillis() + 1000 * 60 * 60 * 2);
}
};
String token = JWTUtil.createToken(map, "softwise".getBytes());
// 把完整的用户信息存入Redis,userId作为key
redisUtil.setCacheObject("login:" + String.valueOf(userId), loginUser);
return new ResponseResult(200, "登录成功", new HashMap<String, String>() {
{
put("token", token);
}
});
}
}
2.3.3.4 认证过滤器
认证过滤器需要继承OncePerRequestFilter,然后重写doFilterInternal方法
package top.cluski.tokendemo.filter;
import cn.hutool.core.util.StrUtil;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import top.cluski.tokendemo.domain.LoginUser;
import top.cluski.tokendemo.utils.RedisUtil;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private RedisUtil redisUtil;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = httpServletRequest.getHeader("token");
if (StrUtil.isBlank(token)) {
// 放行
filterChain.doFilter(httpServletRequest, httpServletResponse);
// 这里必须要return,防止调用回来之后走后面的代码
return;
}
// 解析token
JWT jwt = JWTUtil.parseToken(token);
Object userId = jwt.getPayload("uid");
if (Objects.isNull(userId)) {
throw new RuntimeException("解析token失败");
}
// 从Redis中获取用户信息
LoginUser loginUser = redisUtil.getCacheObject("login:" + String.valueOf(userId));
if (Objects.isNull(loginUser)) {
throw new RuntimeException("Redis获取用户信息失败");
}
// 存入SecurityContextHolder
// TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
然后将我们自定义的过滤器加入到Spring Security过滤器链中(加在UsernamePasswordAuthenticationFilter之前)
还是加在上文中的
SecurityConfig类中的configure方法中。
// 将自己写的JwtAuthenticationTokenFilter添加进Spring Security的filter链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
2.3.3.5 退出登陆
我们只需要定义一个退出登陆的接口,然后获取SecurityContextHolder中的认证信息,删除Redis中的对应的数据即可。
在上文的LoginService加入下面的方法:
@Override
public ResponseResult logout() {
// 获取SecurityContextHolder中的userId
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Long userId = loginUser.getUser().getId();
// 删除Redis中的值
redisUtil.deleteObject("login:" + String.valueOf(userId));
return new ResponseResult(200, "退出成功");
}
3 授权
3.0 授权系统的作用
例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能。
总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。
我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作。
所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须基于所需权限才能进行相应的操作。
3.1 授权的基本流程
在SpringSecurity中,会使用默认的FilterSecuritylnterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。
然后设置我们的资源所需要的权限。
3.2 授权实现
3.2.1 限制访问资源所需权限
SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解制定访问对应资源所需要的权限。
但是要使用它我们需要先开启相关配置
@EnableGlobalMethodSecurity(prePostEnabled = true)
然后就可以使用对应的注解,@PreAuthorize
@RestController
public class HelloController {
@RequestMapping("/hello")
@PreAuthorize("hasAuthority('test')")
public String hello() {
return "hello";
}
}
3.2.2 封装权限信息
我们前面在写UserDetailsServiceImpl的时候说过,在查询出用户还要获取对应的权限信息,封装到UserDetails中返回。
我们先直接把权限写死封装到UserDetails中进行测试。
我们之前定义了UserDetails的实现类LoginUser,想要让其能够封装权限信息,就要对其进行修改。
3.2.2.1 LoginUser
在LoginUser类中添加权限信息(List<String> permissions),并实现getAuthorities方法。
// 用户信息
private User user;
// 权限信息
private List<String> permissions;
// 转换后的权限信息,SpringSecurity所需
// 并且存入Redis的时候,不进行序列化
@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities != null) {
return this.authorities;
}
List<SimpleGrantedAuthority> authorities = permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
this.authorities = authorities;
return authorities;
}
3.2.2.2 UserDetailsServiceImpl
UserDetailsServiceImpl中暂时写死权限
// TODO: 查询对应的权限
List<String> permissions = new ArrayList<>(Arrays.asList("test", "admin"));
return new LoginUser(user, permissions);
3.2.2.3 JwtAuthenticationTokenFilter
JwtAuthenticationTokenFilter中,将权限信息设定进Authentication中
// 存入SecurityContextHolder
// TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(httpServletRequest, httpServletResponse);
3.2.3 从数据库查询权限信息
3.2.3.1 RBAC权限模型
RBAC(Role-Base Access Control)即:基于校色权限控制。这是目前最常被开发者使用的,也是相对易用,通用的权限模型。
3.2.3.2 准备工作
建表语句:
drop table if exists sys_menu;
create table sys_menu
(
menu_id bigint(20) not null auto_increment comment '菜单ID',
menu_name varchar(50) not null comment '菜单名称',
path varchar(200) default '#' comment '请求地址',
component varchar(255) default null comment '组件路径',
visible char(1) default 0 comment '菜单状态(0显示 1隐藏)',
status char(1) default 0 comment '菜单状态(0正常 1停用)',
perms varchar(100) default null comment '权限标识',
icon varchar(100) default '#' comment '菜单图标',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
del_flag tinyint(1) default 0 comment '是否删除(0未删除 1已删除)',
remark varchar(500) default null comment '备注',
primary key (menu_id)
) engine = innodb
auto_increment = 2000 comment = '菜单权限表';
insert into sys_menu value (1, '部门管理', 'dept', 'system/dept/index', 0, 0, 'system:dept:list', '#', 'admin', sysdate(), '', null, 0, '部门管理');
insert into sys_menu value (2, '测试', 'test', 'system/test/index', 0, 0, 'system:test:list', '#', 'admin', sysdate(), '', null, 0, '测试');
drop table if exists sys_role;
create table sys_role
(
role_id bigint(20) not null auto_increment comment '角色ID',
role_name varchar(30) not null comment '角色名称',
role_key varchar(100) not null comment '角色权限字符串',
status char(1) not null comment '角色状态(0正常 1停用)',
del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) default null comment '备注',
primary key (role_id)
) engine = innodb
auto_increment = 100 comment = '角色信息表';
insert into sys_role values('1', '超级管理员', 'admin', '0', '0', 'admin', sysdate(), '', null, '超级管理员');
insert into sys_role values('2', '普通角色', 'common', '0', '0', 'admin', sysdate(), '', null, '普通角色');
drop table if exists sys_role_menu;
create table sys_role_menu
(
role_id bigint(20) not null comment '角色ID',
menu_id bigint(20) not null comment '菜单ID',
primary key (role_id, menu_id)
) engine = innodb comment = '角色和菜单关联表';
insert into sys_role_menu values(1, 1);
insert into sys_role_menu values(1, 2);
create table sys_user
(
id bigint(20) not null auto_increment comment '主键',
user_name varchar(64) not null comment '登录账号',
nick_name varchar(64) not null comment '用户昵称',
password varchar(64) not null comment '密码',
status char(1) default '0' comment '帐号状态(0正常 1停用)',
email varchar(50) default '' comment '用户邮箱',
phone_number varchar(32) default '' comment '手机号码',
sex char(1) default '0' comment '用户性别(0男 1女 2未知)',
avatar varchar(128) default '' comment '头像',
user_type char(1) not null default '1' comment '用户类型(0管理员 01普通用户)',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
del_flag char(1) default '0' comment '删除标志(0代表存在 1代表删除)',
primary key (id)
) engine = innodb
auto_increment = 100 comment = '用户信息表';
drop table if exists sys_user_role;
create table sys_user_role
(
user_id bigint(20) not null comment '用户ID',
role_id bigint(20) not null comment '角色ID',
primary key (user_id, role_id)
) engine = innodb comment = '用户和角色关联表';
insert into sys_user_role value (1, 100);
查询某个用户的权限可以使用以下的sql:
# ----------------------------
# 查询测试
# ----------------------------
select distinct sm.perms
from sys_user_role sur
inner join sys_role sr on sur.role_id = sr.role_id and sr.status = 0
left join sys_role_menu srm on sr.role_id = srm.role_id
inner join sys_menu sm on srm.menu_id = sm.menu_id and sm.status = 0
where sur.user_id = 100
3.2.3.3 代码实现
定义Mapper接口
package top.cluski.tokendemo.mapper;
import top.cluski.tokendemo.domain.SysMenu;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
public interface SysMenuMapper extends BaseMapper<SysMenu> {
List<String> selectPermsByUserId(Long id);
}
mapper.xml中写sql语句
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.cluski.tokendemo.mapper.SysMenuMapper">
<resultMap id="BaseResultMap" type="top.cluski.tokendemo.domain.SysMenu">
<id property="menuId" column="menu_id" jdbcType="BIGINT"/>
<result property="menuName" column="menu_name" jdbcType="VARCHAR"/>
<result property="path" column="path" jdbcType="VARCHAR"/>
<result property="component" column="component" jdbcType="VARCHAR"/>
<result property="visible" column="visible" jdbcType="CHAR"/>
<result property="status" column="status" jdbcType="CHAR"/>
<result property="perms" column="perms" jdbcType="VARCHAR"/>
<result property="icon" column="icon" jdbcType="VARCHAR"/>
<result property="createBy" column="create_by" jdbcType="VARCHAR"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateBy" column="update_by" jdbcType="VARCHAR"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="delFlag" column="del_flag" jdbcType="TINYINT"/>
<result property="remark" column="remark" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
menu_id,menu_name,path,
component,visible,status,
perms,icon,create_by,
create_time,update_by,update_time,
del_flag,remark
</sql>
<select id="selectPermsByUserId" resultType="java.lang.String">
select distinct sm.perms
from sys_user_role sur
inner join sys_role sr on sur.role_id = sr.role_id and sr.status = 0
left join sys_role_menu srm on sr.role_id = srm.role_id
inner join sys_menu sm on srm.menu_id = sm.menu_id and sm.status = 0
where sur.user_id = #{id}
</select>
</mapper>
UserDetailsServiceImpl中添加从mapper查询
// 从数据库中查询权限信息
List<String> permissions = sysMenuMapper.selectPermsByUserId(user.getId());
// List<String> permissions = new ArrayList<>(Arrays.asList("test", "admin"));
return new LoginUser(user, permissions);
4 自定义失败处理
我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
4.1 自定义实现类
认证失败
package top.cluski.tokendemo.handler;
import com.alibaba.fastjson.JSON;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import top.cluski.tokendemo.domain.ResponseResult;
import top.cluski.tokendemo.utils.WebUtil;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "用户认证失败,请重新登录");
String jsonStr = JSON.toJSONString(result);
// 处理异常
WebUtil.renderString(httpServletResponse, jsonStr);
}
}
访问拒绝
package top.cluski.tokendemo.handler;
import com.alibaba.fastjson.JSON;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import top.cluski.tokendemo.domain.ResponseResult;
import top.cluski.tokendemo.utils.WebUtil;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "您的权限不足");
String jsonStr = JSON.toJSONString(result);
// 处理异常
WebUtil.renderString(httpServletResponse, jsonStr);
}
}
4.2 配置SpringSecurity
在上文中的SecurityConfig类的configure方法中,添加异常处理器的配置:
// 配置异常处理器
// 认证失败
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
// 授权失败
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
5 跨域
浏览器出于安全的考虑,使用XMLHttpRequest对象发起HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。
前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。
所以我们就要处理一下,让前端能进行跨域请求。
① 先对SpringBoot配置,运行跨域请求
package top.cluski.tokendemo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求域名
.allowedOriginPatterns("*")
// 设置允许Cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的Header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
② 开启SpringSecurity的跨域访问
在上文中的SecurityConfig类的configure方法中,添加跨域的配置:
// 允许跨域
http.cors();
6 遗留的问题
6.1 其它权限校验方法
我们前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法进行校验。SpringSecurity还为我们提供了其它方法例如:hasAnyAuthority,hasRole,hasAnyRole,等。
这里我们先不急着去介绍这些方法,我们先去理解hasAuthority的原理,然后再去学习其他方法你就更容易理解,而不是死记硬背区别。并且我们也可以选择定义校验方法,实现我们自己的校验逻辑。
hasAuthority方法实际是执行到了SecurityExpressionRoot的hasAuthority,大家只要断点调试既可知道它内部的校验原理。它内部其实是调用authentication的getAuthorities方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。
hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。
hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。
hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。
6.2 自定义权限校验方法
我们也可以定义自己的权限校验方法,在@PreAuthorize注解中使用我们的方法。
① 添加我们自己的验证类,并注入到IOC中
package top.cluski.tokendemo.expression;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import top.cluski.tokendemo.domain.LoginUser;
import java.util.HashSet;
import java.util.Set;
@Component
public class MyExpressionRoot {
public boolean hasAuthority(String authority) {
// 获取当前用户的权限
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Set<String> permissions = new HashSet<>(loginUser.getPermissions());
// 校验权限
return permissions.contains(authority);
}
}
② Spring-EL表达式用指定使用我们自己定义好的类,如@PreAuthorize("@myExpressionRoot.hasAuthority('system:test:list')")。
6.3 基于配置的权限控制
我们也可以在配置类中使用配置的方式对资源进行权限的控制。配置SecurityConfig的configure方法。

6.4 CSRF
CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。https://blog.csdn.net/freeking101/article/details/86537087
SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。
我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。
6.5 认证成功处理器
实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候,如果登录成功了是会调用AuthenticationSuccessHandler的方法进行认证成功后的处理的。AuthenticationSuccessHandler就是登录成功处理器。
我们也以自己去自定义成功处理器进行成功后的相应处理。
① 实现AuthenticationSuccessHandler
package com.example.securityquickstart.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class MySuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("认证成功!");
}
}
② 配置Spring Security
package com.example.securityquickstart.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.annotation.Resource;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().successHandler(authenticationSuccessHandler)
.and()
.authorizeRequests().anyRequest().authenticated();
}
}
6.6 认证失败处理器
实际上在UsernamePasswordAuthenticationFilter进行认证登录的时候,如果登录失败了是会调用AuthenticationFailureHandler的方法进行认证失败后的处理的。AuthenticationFailureHandler就是登录成功处理器。
我们也以自己去自定义失败处理器进行成功后的相应处理。
① 实现AuthenticationFailureHandler
package com.example.securityquickstart.handler;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class MyFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("认证失败!");
}
}
② 配置Spring Security
package com.example.securityquickstart.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.annotation.Resource;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Resource
private AuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
// 配置认证成功处理器
.successHandler(authenticationSuccessHandler)
// 配置认证失败处理器
.failureHandler(authenticationFailureHandler)
.and()
.authorizeRequests().anyRequest().authenticated();
}
}
6.7 登出成功处理器
① 实现LogoutSuccessHandler
package com.example.securityquickstart.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("注销成功");
}
}
② 配置Spring Security
package com.example.securityquickstart.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.annotation.Resource;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private LogoutSuccessHandler logoutSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().authenticated();
http
.logout()
// 配置注销成功处理器
.logoutSuccessHandler(logoutSuccessHandler);
}
}
评论区