SpringSecurity+JWT认证流程解析 | 掘金新人第一弹 - 掘金
SpringBoot 整合SpringSecurity示例实现前后分离权限注解+JWT登录认证-阿里云开发者社区
20.配置认证过滤器_哔哩哔哩_bilibili
15-尚硅谷-SpringSecurity-web权限方案-用户授权(注解使用)_哔哩哔哩_bilibili
和耳朵/spring-boot-learning-demo
核心:过滤器链
SpringSecurity官方文档中有这么一句话:
Spring Security’s web infrastructure is based entirely on standard servlet filters.
也就是说,SpringSecurity的核心基础就是Servlet过滤器链
一个Web请求会通过一条完整的过滤器链,在经过过滤器链的时候对完成认证和授权,如果中间发现这条请求未认证或未授权,会根据被保护API的权限去抛出异常,然后由异常处理器去处理这些异常。
上图中的UsernamePasswordAuthenticationFilter和BasicAuthenticationFilter对应着config配置类中的formLogin和httpBasic的配置项,在配置项如果对这两个属性进行了配置,那么就会将上述两个过滤器加载到我们的过滤器链中
formLogin对应着你form表单认证方式,即UsernamePasswordAuthenticationFilter。httpBasic对应着Basic认证方式,即BasicAuthenticationFilter。
重要概念
- SecurityContext:上下文对象,会存放
Authentication对象 - SecurityContextHolder:用于拿到上下文对象的静态工具类
- Authentication:认证接口,定义了认证对象的数据形式
AuthenticationManager:用于校验
Authentication,返回一个认证完成后的Authentication对象1. SecurityContext

其中只有两个方法,对Authenticationset 或者 get2. SecurityContextHolder

相当于是SecurityContext的工具类,可以用来获取或者清除SecurityContext3. Authentication

几个方法的作用如下:getAuthorities:获取用户权限,一般情况下获取到的是用户的角色信息getCredentials:获取证明用户认证的信息,一般获取到的是密码等信息getDetails:获取用户的额外信息getPrincipal:获取用户身份信息,在未认证的情况下获取到的是用户名,已认证的情况下获取到的是UserDetailsisAuthenticated:获取当前Authentication 是否已完成认证setAuthenticated:设置当前Authentication是否已认证4. AuthenticationManager
public interface AuthenticationManager {// 认证方法Authentication authenticate(Authentication authentication)throws AuthenticationException;}
AuthenticationManager定义了一个认证方法,它将一个未认证的Authentication传入,返回一个已认证的Authentication,默认使用的实现类为:ProviderManager。
那么,如何将上述四个部分串联起来,从而构建出基于SpringSecurity的登录认证流程?
- ✔️先是一个请求,携带着用户信息进来
- ✔️通过
AuthenticationManager或者AuthenticationProvider的认证 - ✔️然后通过
SecurityContextHolder获取SecurityContext - ✔️将认证的信息放到
SecurityContext中
自定义登录逻辑处理器
package com.sheep.securitylearning.handler;import cn.hutool.log.Log;import cn.hutool.log.LogFactory;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import com.sheep.securitylearning.entity.Users;import com.sheep.securitylearning.mapper.UserMapper;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.authentication.AuthenticationProvider;import org.springframework.security.authentication.BadCredentialsException;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.Authentication;import org.springframework.security.core.AuthenticationException;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.AuthorityUtils;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.stereotype.Component;import java.util.List;import java.util.Objects;/*** Created By Intellij IDEA** @author ssssheep* @package com.sheep.securitylearning.handler* @datetime 2022/8/8 星期一*/@Componentpublic class UserAuthenticationProvider implements AuthenticationProvider {private static final Log log = LogFactory.get(UserAuthenticationProvider.class);@Autowiredprivate UserMapper userMapper;@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {log.info("on authenticate");// 获取表单输入中返回的用户名String username = (String) authentication.getPrincipal();// 获取表单中输入的密码String password = (String) authentication.getCredentials();QueryWrapper<Users> queryWrapper = new QueryWrapper<>();queryWrapper.eq("username", username);Users users = userMapper.selectOne(queryWrapper);if (Objects.isNull(users)) {throw new UsernameNotFoundException("用户名不存在");}if (!users.getPassword().equalsIgnoreCase(password)) {throw new BadCredentialsException("密码不正确");}List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admins");return new UsernamePasswordAuthenticationToken(users, password, auths);}@Overridepublic boolean supports(Class<?> authentication) {return true;}}
封装TokenUtil
由于我们前后端分离需要基于JWT来进行认证,因此我们需要自行封装一个帮我们操作Token的工具类,主要需要以下三个方法
- 生成token
- 验证token
- 反解析token ```java package com.sheep.securitylearning.utils;
import cn.hutool.core.lang.UUID; import cn.hutool.json.JSONUtil; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; import org.apache.tomcat.util.codec.binary.Base64; import org.springframework.security.core.userdetails.UserDetails;
import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Date; import java.util.HashMap;
/**
- Created By Intellij IDEA *
- @author ssssheep
- @package com.sheep.securitylearning.utils
@datetime 2022/8/8 星期一 */ public class JwtUtil {
private static final Long DEFAULT_EXPIRATION_TIME = 259200000L;
public static SecretKey generalKey() {
String stringKey = "ssssheep";byte[] decodedKey = Base64.decodeBase64(stringKey);return new SecretKeySpec(decodedKey, "AES");
}
public static String createKey(String username, Long ttl) {
if (ttl <= 0) {ttl = DEFAULT_EXPIRATION_TIME;}SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);HashMap<String, Object> claims = new HashMap<>();claims.put("username", username);SecretKey key = generalKey();return Jwts.builder().setClaims(claims).setId(UUID.randomUUID().toString()).setIssuedAt(now).setIssuer("yxr").setSubject(JSONUtil.toJsonStr(claims)).signWith(signatureAlgorithm, key).setExpiration(new Date(nowMillis + DEFAULT_EXPIRATION_TIME)).compact();
}
public static boolean isTokenExpired(Claims claims) {
return claims.getExpiration().before(new Date());
}
/**
- 验证token是否合法 *
- @param token token值
- @param userDetails 用户信息
- @return 是否合法
*/
public static boolean checkValid(String token, UserDetails userDetails) {
Claims claims = parseKey(token);
System.out.println(claims.getSubject());
String username = JSONUtil.parseObj(claims.getSubject()).getStr(“username”);
return username.equals(userDetails.getUsername())
}&& !isTokenExpired(claims);
public static Claims parseKey(String token) throws SignatureException {SecretKey key = generalKey();return Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();}
// public static void main(String[] args) { // String token = “eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJpc3NcIjpcInl4clwiLFwiaWF0XCI6MTY2MDAxMjY3NSxcImp0aVwiOlwiZTY2YzlmZmYtYzRjYi00NDU5LWJhOWQtMmQzZTZlZTBmNDE3XCIsXCJ1c2VybmFtZVwiOlwiYWRtaW5cIn0iLCJpc3MiOiJ5eHIiLCJleHAiOjE2NjAyNzE4NzUsImlhdCI6MTY2MDAxMjY3NSwianRpIjoiZTY2YzlmZmYtYzRjYi00NDU5LWJhOWQtMmQzZTZlZTBmNDE3IiwidXNlcm5hbWUiOiJhZG1pbiJ9.P2j2CgQNrDOLdA7HtYUPBFu2DT91wDAFYQj1rWpZXv8”; // Users users = new Users(1, “admin”, “123”); // System.out.println(checkValid(token, users)); // } }
<a name="WuQ1i"></a>## 编写请求登录接口访问任何一个系统,最先访问的就是认证方法,如下是根据最简单的登录逻辑编写了一个登录接口```java@GetMapping("/login")public ApiResult login(String username, String password) {UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);Authentication authentication = userAuthenticationProvider.authenticate(authenticationToken);SecurityContextHolder.getContext().setAuthentication(authentication);Users principal = (Users) authentication.getPrincipal();String token = JwtUtil.createKey(principal.getUsername(), -1L);HashMap<String, Object> result = new HashMap<>();result.put("token", token);return ApiResult.success("登录成功", result);}
- 🌈根据传入的用户名和密码构建一个
UsernamePasswordAuthenticationToken - 🌈将
UsernamePasswordAuthenticationToken传入我们的自定义登录逻辑处理器中,返回一个Authentication对象 - 🌈如果认证成功,那么就会走到第三步。就可以通过
SecurityContextHolder获取SecurityContext,然后将认证之后的Authentication对象,放入上下文对象中 - 🌈从
Authentication对象中拿到我们的UserDetails,然后根据封装的JwtUtil创建token,返回给前端
🗝️将token返回给前端后,前端之后的每次请求都要在请求头中携带上token
JWT过滤器
package com.sheep.securitylearning.filter;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import com.sheep.securitylearning.config.ApiResult;import com.sheep.securitylearning.entity.Users;import com.sheep.securitylearning.mapper.UserMapper;import com.sheep.securitylearning.utils.JwtUtil;import com.sheep.securitylearning.utils.ResultUtil;import io.jsonwebtoken.Claims;import io.jsonwebtoken.SignatureException;import lombok.RequiredArgsConstructor;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.AuthorityUtils;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.stereotype.Component;import org.springframework.util.StringUtils;import org.springframework.web.filter.OncePerRequestFilter;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.List;import java.util.Objects;/*** Created By Intellij IDEA** @author ssssheep* @package com.sheep.securitylearning.filter* @datetime 2022/8/8 星期一*/@Component@RequiredArgsConstructorpublic class JwtAuthTokenFilter extends OncePerRequestFilter {final UserMapper userMapper;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {System.out.println("进入了JwtAuthTokenFilter");// 获取tokenString token = request.getHeader("token");if (!StringUtils.hasText(token)) {filterChain.doFilter(request, response);return;}// 解析tokenClaims claims;try {claims = JwtUtil.parseKey(token);} catch (SignatureException e) {e.printStackTrace();ResultUtil.responseJson(response, ApiResult.error("token已失效"));return;}Integer uid = claims.get("uid", Integer.class);// 从数据库中获取用户信息QueryWrapper<Users> queryWrapper = new QueryWrapper<>();queryWrapper.eq("id", uid);Users users = userMapper.selectOne(queryWrapper);if (Objects.isNull(users)) {ResultUtil.responseJson(response, ApiResult.notLogin());return;}// 判断token是否合法if (!JwtUtil.checkValid(token, users)) {ResultUtil.responseJson(response, ApiResult.notLogin());return;}// 存储SecurityContextHolderList<GrantedAuthority> auths= AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admins");UsernamePasswordAuthenticationToken authenticationToken= new UsernamePasswordAuthenticationToken(users, users.getPassword(), auths);SecurityContextHolder.getContext().setAuthentication(authenticationToken);// 放行filterChain.doFilter(request, response);}}
在过滤器中主要是获取请求头中的token并且验证其是否合法
判断token合法后,获取用户的信息并组装好一个authentication对象,将它放在上下文的对象中,这样后面的过滤器就可以获取authentication对象,就相当于已经认证过了
对Security进行配置
package com.sheep.securitylearning.config;import com.sheep.securitylearning.filter.JwtAuthTokenFilter;import com.sheep.securitylearning.handler.*;import lombok.RequiredArgsConstructor;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;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;import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;/*** Created By Intellij IDEA** @author ssssheep* @package com.sheep.securitylearning.config* @datetime 2022/8/8 星期一*/@Configuration@RequiredArgsConstructor@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)@EnableWebSecuritypublic class SecurityConfig extends WebSecurityConfigurerAdapter {final JwtAuthTokenFilter jwtAuthTokenFilter;final UserLogoutSuccessHandler userLogoutSuccessHandler;final UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;final UserAuthenticationProvider userAuthenticationProvider;final UserAuthenticationEntryPoint userAuthenticationEntryPoint;@Beanpublic DefaultWebSecurityExpressionHandler userSecurityExpressionHandler() {DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();handler.setPermissionEvaluator(new UserPermissionEvaluator());return handler;}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests().antMatchers("/user/login").anonymous()//除了登录接口以外的所有接口都需要认证访问.anyRequest().authenticated().and().formLogin().disable().logout().logoutSuccessHandler(userLogoutSuccessHandler).and().exceptionHandling().authenticationEntryPoint(userAuthenticationEntryPoint).accessDeniedHandler(userAuthAccessDeniedHandler).and().cors().and().csrf().disable();http.headers().cacheControl();// 将我们自定义的JWT过滤器加入到过滤器链中http.addFilterBefore(jwtAuthTokenFilter, UsernamePasswordAuthenticationFilter.class);}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.authenticationProvider(userAuthenticationProvider);}@BeanPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
测试
准备三个接口

