SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
Spring Security一、简介Spring Security是Spring家族中的一个安全管理框架,一般Web应用都需要 认证 和 授权认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户授权:经过认证后判断当前用户是否有权限进行某个操作二、快速入门2.1 开发步骤1、导入坐标Spring Security 启动器<dependency><groupId&
Spring Security
一、简介
Spring Security是Spring家族中的一个安全管理框架,一般Web应用都需要 认证 和 授权
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
二、快速入门
2.1 开发步骤
1、导入坐标
Spring Security 启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2、随便编写一个接口
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/login")
public String login() {
return "Spring Security";
}
}
3、启动
可以看到,Spring Security已经起作用了,默认的用户名是:user,而密码则在控制台上
当我们输入用户名密码后,就能访问到我们的接口数据了~
三、认证
3.1 登录校验流程
在一般的开发场景中,登录校验的流程大致如下:
1. 前端携带一个 用户名和密码 访问登录接口;
2. 后端通过数据库校验用户名和密码;
3. 如果用户名密码正确,则使用 User的部门信息(用户名、用户ID)生成一个JWT;
4. 将JWT响应给前端;
5. 前端登录后,每次发送请求都会携带Token;
6. 后端获取请求头中的Token进行解析,获取UserID;
7. 根据UserID获取用户相关信息,如果有权限则允许访问资源;
8. 访问到目标资源,响应给前端。
3.2 Spring Security完整流程
Spring Security的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器
1、UsernamePasswordAuthenticationFilter,负责处理登录页面填写了用户名密码后的登录请求。
2、ExceptionTranslationFilter:处理过滤器链中抛出的任何 AccessDeniedException
3、FilterSecurityInterceptor:负责权限校验的过滤器
3.3 解决问题
一、思路
登录:
1. 自定义登录接口
调用ProviderManager的方法进行认证 如果认证通过生成JWT
把用户信息存入Redis
2. 自定义UserDetailService
在这个实现列中去查询数据库
校验:
1. 定义JWT认证过滤器
获取Token
解析Token获取其中的UserID
从Redis中获取用户信息
存入SecurityContextHolder
二、实现方式
-
导入坐标
<dependencies> <!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Spring Boot Web的启动类 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 处理Json --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.78</version> </dependency> <!-- Redis相关 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- Redis连接池相关 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!-- jjwt --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!-- 数据库相关 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Java开发必备的万能工具包 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.22</version> </dependency> </dependencies>
-
统一响应体
@Data @NoArgsConstructor @AllArgsConstructor public class Result { private Boolean success; private String errorMsg; private Object data; private Long total; public static Result ok(){ return new Result(true, null, null, null); } public static Result ok(Object data){ return new Result(true, null, data, null); } public static Result ok(List<?> data, Long total){ return new Result(true, null, data, total); } public static Result fail(String errorMsg){ return new Result(false, errorMsg, null, null); } }
-
编写实体类
@Data public class User implements Serializable { private Long id; private LocalDateTime birthday; private String gender; private String username; private String password; private String telephone; private String station; private String remark; }
-
编写
UserMapper.java
接口@Mapper @Repository public interface UserMapper { List<User> findAll(); }
-
编写
UserService.java
接口public interface UserService { List<User> findAll(); }
-
编写
UserServiceImpl.java
实现类@Service public class UserServiceImpl implements UserService { @Resource private UserMapper userMapper; @Override public List<User> findAll() { return userMapper.findAll(); } }
-
编写
UserMapper.xml
映射配置文件<?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="com.rabbit.springbootsecurity.mapper.UserMapper"> <select id="findAll" resultType="user"> select * from t_user </select> </mapper>
-
编写
application.yaml
配置文件server: port: 8083 spring: datasource: url: jdbc:mysql://localhost:3306/health?characterEncoding=utf-8&serverTimezone=UTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver #MyBatis mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.rabbit.springbootsecurity.pojo #别名
-
编写
UserController.java
@RestController @RequestMapping("/user") public class UserController { @Resource private UserService userService; @GetMapping("/login") public Result login() { return Result.ok(userService.findAll()); } }
测试结果:
三、存在问题
以上的入门案例存在一定问题:
我们每次访问接口都需要使用到 SpringSecurity 为我们提供的默认 用户名(user)和密码(控制台有),这显然不是我们想要的,所以我们需要创建一个类,实现 UserDeatailsService接口,重写其中的方法 loadUserByUsername() ,表示用户名密码从数据库中查询。
问题解决:
-
编写一个类,
UserDetailsServiceImpl.java
,实现UserDetailsService
接口,重写里边的方法,表示我们需要从数据库中获取用户信息@Component public class UserDetailsServiceImpl implements UserDetailsService { @Resource private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 根据用户名查询用户信息 User user = userMapper.findUserByUsername(username); /** * 若查询不到用户信息,则抛异常 * SpringSecurity可以处理我们在查询中遇到的异常 */ if (Objects.isNull(user)) { throw new RuntimeException("用户名或密码错误"); } // TODO 根据用户查询权限信息,添加到LoginUser中 // 因为返回值是UserDetails,所有需要定义一个类,实现UserDetails,把用户信息封装在其中 return new LoginUser(user); } }
-
可以看到,实现的类中重写的方法返回值是
UserDetails
,所以我们需要编写一个类,实现UserDetails
接口并重写里边的方法@Data @NoArgsConstructor @AllArgsConstructor 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; } }
注意:
如果要测试,若数据库中的密码是明文存储,需要在数据库的密码列的值前边加上 `{noop}`,表示是明文存储
四、密码加密解密
实际项目中,我们不会把密码明文的存储到数据库中;
我们一般会使用Spring Security为我们提供的 BCryptPasswordEncoder;
我们只需要把 BCryptPasswordEncoder对象注入到Spring容器中,Spring Security就会使用该PasswordEncoder来进行密码校验,所以我们可以定义一个Spring Security配置类,该配置类需要继承 WebSecurityConfigurerAdapter。
使用步骤:
-
编写一个配置类,继承
WebSecurityConfigurerAdapter
类该类就是一个配置类,该配置类提供一个方法,需要返回一个 BCryptPasswordEncoder 对象
-
编写一个返回 BCryptPasswordEncoder Bean对象的方法
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 创建BCryptPasswordEncoder注入容器 * @return */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
测试加密方法:
@Test
void testBCryptPasswordEncoder() {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
/**
* encode():加密方法,传入一个明文,加密后返回一个密文
* 同一明文,每次调用encode()方法生成出来的密文都是不一样的,
* 因为内部进行加密的时候,会生成一个【随机的加密盐】,
* 底层是通过【加密盐】和原文进行一系列处理之后再进行加密
* 这样的话,虽然明文一样,但是每一次的密文都是不一样的
*/
String encode_pwd_1 = passwordEncoder.encode("rabbit");
String encode_pwd_2 = passwordEncoder.encode("rabbit");
log.info("encode_pwd_1:{}", encode_pwd_1);
log.info("encode_pwd_2:{}", encode_pwd_2);
}
测试校验方法:
@Test
void testBCryptPasswordEncoder() {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
boolean flag_true = passwordEncoder.
matches("rabbit", "$2a$10$MBcThIW7Tqm9liaBAXooPepAeovbD8XbM1tV3xvHOA6FHaI6vD4hO");
boolean flag_false = passwordEncoder.
matches("root", "$2a$10$MBcThIW7Tqm9liaBAXooPepAeovbD8XbM1tV3xvHOA6FHaI6vD4hO");
log.info("flag_true:{}", flag_true);
log.info("flag_false:{}", flag_false);
}
五、Jwt的使用和使用Jwt进行登录
5.1 什么是Jwt
Jwt 是 JSON WEB TOKEN 英文的缩写,它是一个开放标准,它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全传输信息。该信息可以被验证和信任,因为它是数字签名用的。
使用jwt最多的场景就是登陆,一旦用户登陆,那么后续的每个请求都应该包含jwt。
5.2 Jwt 组成
Jwt由三部分组成,每一部分之间用符号"."进行分割,整体可以看做是一个长字符串。
一个经典的jwt的样子:
xxx.xxx.xxx
Jwt的三部分是:
-
Header 头部:
-
头部由两部分组成,第一部分是声明类型,在 Jwt 中声明类型就 Jwt,第二部门就是声明加密算法,一般是话是采用
Hash256
-
一个经典的头部:
{ 'typ': 'JWT', // 'typ':'声明类型' 'alg': 'HS256' // 'alg':'声明的加密算法' }
-
-
Payload 载荷、载体
-
这一部分是 Jwt 的主体部分,这一部分也是 Json 对象,可以包含需要传递的数据,其中 Jw t指定了七个默认的字段选择,这七个字段是推荐但是不强制使用的:
iss:发行人 exp:到期时间 sub:主题 aud:用户 nbf:在此之前不可用 iat:发布时间 jti:JWT ID用于识别该JWt
-
除了上述的七个默认字段之外,还可以自定义字段,通常我们说 Jwt 用于用户登陆,就可以在这个地方放置 userId、username
-
下面这个Json对象是一个 Jwt 的 Payload 部分:
{ "sub": "1234567890", "nickname": "rabbit", "id": "9527" }
注意:这里不能存放敏感信息,否则可能会被截取
-
-
signature 签证:
-
这部分是 对前两部分进行 base64 编码 再进行加密,这个加密的方式使用的是 Jwt的头部声明中的加密方式,在加上一个加密盐(secret)组成的,secret通常是一个随机的字符串,这个secret是服务器特有的,不能够让其他人知道。这部分的组成公式是:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)
-
5.3 为什么选择 Jwt
-
session 的缺点:
首先在我的认知里 Jwt 用处最多的就是作为用户登陆的凭证,以往这个凭证是使用session和cookie进行存储的,session技术的存储在服务器端的一种技术,构造一个类似于哈希表存储用户id和用户的一些信息,将这个用户id放在cookie里返回给用户,用户每次登陆的时候带上这个cookie,在哈希表中如果可以查到信息,那么说明用户登陆并且得到对应用户的信息; 但是session存放在服务器端,当用户量很大时,占用了服务器过多的宝贵的内存资源。同时因为如果有多台服务器,那么当用户登陆时访问了服务器A,那么就只有服务器A上会存储这个用户的信息,当用户访问其他页面时,也许请求会发给服务器B,这时服务器B中是没有用户的信息的,会判定用户处于非登录的状态。也就是说session无法很好的在微服务的架构之中使用; 因为session是和cookie结合使用的,如果cookie被截获,那么就会存在安全危机。
-
Jwt 的优点:
Json形式,而Json非常通用性可以让它在很多地方使用; Jwt所占字节很小,便于传输信息; 需要服务器保存信息,易于扩展;
5.4 一个 Jwt 的工具类
package com.rabbit.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "rabbit";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("sg") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}
/**
* 创建token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
return builder.compact();
}
public static void main(String[] args) throws Exception {
// String jwt = createJWT("2123");
Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyOTY2ZGE3NGYyZGM0ZDAxOGU1OWYwNjBkYmZkMjZhMSIsInN1YiI6IjIiLCJpc3MiOiJzZyIsImlhdCI6MTYzOTk2MjU1MCwiZXhwIjoxNjM5OTY2MTUwfQ.NluqZnyJ0gHz-2wBIari2r3XpPp06UMn4JS2sWHILs0");
String subject = claims.getSubject();
System.out.println(subject);
// System.out.println(claims);
}
/**
* 生成加密后的秘钥 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
六、登录接口
在实际开发中,我们通常有的接口,不需要登录也能访问,比如:登录页面、注册、忘记密码等,所以我们需要在 Spring Security配置一些信息以及编写一些放行的方法:
在接口中我们通过 AuthenticationManager 的 authenticate() 方法来进行用户认证,所以需要在 SecurityConfig 中配置把 AuthenticationManager 注入Spring IoC容器
认证成功的话,需要生成一个 JWT,放入响应中返回。并且为了让用户下回请求时能通过 JWT 识别处具体是哪个用户,我们需要把用户信息存入Redis
6.1 准备工作
-
创建一个用于存放 Redis 的 Key 名称的类
public class RedisConstants { public static final String LOGIN_USER_KEY = "jwt:login:token:"; public static final Long LOGIN_USER_TTL = 36000L; }
-
创建一个用于存放登录信息的实体类
LoginDto.java
@Data @AllArgsConstructor @NoArgsConstructor public class LoginDto implements Serializable { private String username; private String password; private String telephone; }
-
创建一个实体类,用于保存部分信息至Redis的
UserDto.java
@Data @AllArgsConstructor @NoArgsConstructor public class UserDto implements Serializable { private Long id; private String username; }
6.2 开发步骤
-
在开发中,我们不可能对任何请求都进行拦截,肯定是部分请求不要拦截,所以我们需要在
SecurityConfig.java
中重写configure(http)
方法,配置对哪些请求不拦截/** * 对一些接口放行 比如:登录 * @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/loginByPassword").anonymous() .antMatchers("/user/loginByPhone").anonymous() // 除了上面的请求,任何请求都需要鉴权认证 .anyRequest().authenticated(); }
-
使用
AuthenticationManager
进行用户认证,将用户登录的 username、password 封装成Authentication
对象,随后使用authenticationManager
帮助我们完成认证操作,而authentcationManager
最终调用authentcate()
方法完成认证-
在
SecurityConfig.java
中,重写方法authenticationManagerBean()
/** * 重写该方法,暴露 authenticationManagerBean * 具体使用只需保留原本的代码即可 * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
-
在
UserServiceImpl.java
中实现登录接口/** * 根据用户名密码进行登录 * @param loginDto * @return */ @Override public Result loginByPassword(LoginDto loginDto) { // AuthenticationManager 进行用户认证 // 将用户登录的username和password封装成 Authentication 对象 // 随后使用 authenticationManager 帮助我们完成认证操作 // 而 authenticationManager 最终调用 authenticate() 方法完成认证 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); // 如果认证没通过 则给出相应提示 if (ObjectUtil.isEmpty(authenticate)) { log.info("message:{}",ResultMessage.LOGIN_FAIL); throw new RuntimeException("用户名或密码错误!"); } // 认证通过,使用 userId 生成一个 Jwt,Jwt存入Result返回 LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String user_id = loginUser.getUser().getId().toString(); User user = loginUser.getUser(); String jwtToken = JwtUtils.getJwtToken(user_id); // TODO 使用 StringRedisTemplate存储实体类型,用hash 不过需将实体转为Map UserDto userDto_redis = BeanUtil.copyProperties(user, UserDto.class); // TODO 由于 StringRedisTemplate要求所有字段都是String类型,而UserDto的id是long类型 // TODO 所以需要把所有字段都修改为String类型 Map<String, Object> map = BeanUtil.beanToMap(userDto_redis, new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true) .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); // 把用户信息存储到Redis中 userid作为Key的前缀 String login_key = RedisConstants.LOGIN_USER_KEY + user_id; stringRedisTemplate.opsForHash().putAll(login_key, map); // TODO 5.4 设置有效期 stringRedisTemplate.expire(login_key, RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS); Map<String, String > hashMap = new HashMap<>(); hashMap.put("authorization", jwtToken); return Result.ok(hashMap); }
-
6.3 总结
1. 自定义登录接口,让 Spring Security 对这个接口放行,即让用户不需要登录也能进行访问,对应的也就是在 SecurityConfig.java 中通过继承 WebSecurityConfigurerAdapter 类重写 configure(http) 方法然后进行一些必要的配置;
2. 通过在 SecurityConfig.java 中重写 authenticationManagerBean() 方法,暴露 authenticationManagerBean,随后在 UserServiceImpl中国注入该Bean,表示我们可以通过 authenticate()方法进行用户认证;
3. 认证成功的话就可以生成一个Jwt,放入响应中返回,并且为了让用户下回请求时能通过Jwt识别出具体是哪个用户,我们需要把用户信息存储到Redis中
七、校验Token
7.1 简介
前面我们已经生成了一个Token,所以接下来我们需要进行一些必要的Jwt解析认证,流程如下:
1. 定义一个Token解析过滤器,我们使用Spring为我们提供的过滤器接口,原生的Filter过滤器接口在不同的Tomcat版本中存在一点问题;
2. 获取Token;
3. 解析Token获取其中的UserId;
4. 根据UserID获取Redis中的用户信息;
5. 讲用户信息存入SecurityContextHolder,我们前面了解到,Spring Security其实就是一整个过滤器链,所以我们需要把用户认证信息存储到其中一个过滤器,然后后面的过滤器会去该过滤器中查找有无用户信息,有的话才会放行
7.2 编写 Token 解析过滤器
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 定义 Token 解析过滤器
* @param request
* @param response
* @param filterChain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 1. 获取Token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
// 没有获取到Token 放行,让其它过滤器去拦截
filterChain.doFilter(request, response);
return;
}
// 2. 解析Token 获取userID
String userId = null;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Token异常!");
}
// 3. 从Redis中取出用户数据
String redis_key = RedisConstants.LOGIN_USER_KEY + userId;
Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(redis_key);
if (map.isEmpty()) {
throw new RuntimeException("Token异常!");
}
// TODO 从Redis中获取的是hash结构 需要转为实体
UserDto userDto = BeanUtil.fillBeanWithMap(map, new UserDto(), false);
// TODO 4. 将用户信息存入SecurityContextHolder
/**
* setAuthentication()方法需要一个authentication对象,
* 所以我们需要把用户信息封装到authentication中
*/
// TODO 获取权限信息封装到 authentication 中(第三个参数)
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDto,null,null);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 5. 放行
filterChain.doFilter(request, response);
}
}
7.3 配置过滤器链
简介:
前面我们已经编写好了 Spring Security 的Token解析过滤器,但我们还需去配置过滤器,并且我们把过滤器配置好后还需指定其在Spring容器中的顺序,所以需将我们前面写好的解析Token的过滤器配置到用户认证授权的过滤器UsernamePasswordAuthenticationFilter前面。
在 SecurityConfig.java 中的 configure() 方法中配置 http.addFilterBefore()
步骤:
-
在
SecurityConfig.java
中注入自定义的解析Token过滤器@Resource private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
-
把该过滤器放到用户认证拦截器之前
/** * 对一些接口放行 比如:登录 * @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/loginByPassword").anonymous() .antMatchers("/user/loginByPhone").anonymous() // 除了上面的请求,任何请求都需要鉴权认证 .anyRequest().authenticated(); http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); }
八、退出登录
流程分析
我们只需要从 SecurityContextHolder 中获取用户ID,然后根据用户ID删除Redis中的内容即可
代码实现
/**
* 退出登录
* @return
*/
@Override
public Result logout() {
/**
* 1. 从 SecurityContextHolder 中获取用户信息(ID)
* 2. 从Redis中删除对应的Key
*/
UsernamePasswordAuthenticationToken authenticationToken
= (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
UserDto userDto = (UserDto) authenticationToken.getPrincipal();
Long userId = userDto.getId();
// 根据Key删除Redis的值
stringRedisTemplate.delete(RedisConstants.LOGIN_USER_KEY + userId);
return Result.ok("退出成功~");
}
九、总结
Authentication在我看来就是一个载体,在未得到认证之前它用来携带登录的关键参数,比如用户名和密码、验证码;在认证成功后它携带用户的信息和角色集。
交给AuthenticationManager认证。
更多推荐
所有评论(0)