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
其中只有两个方法,对Authentication
set 或者 get2. SecurityContextHolder
相当于是SecurityContext
的工具类,可以用来获取或者清除SecurityContext
3. Authentication
几个方法的作用如下:getAuthorities
:获取用户权限,一般情况下获取到的是用户的角色信息getCredentials
:获取证明用户认证的信息,一般获取到的是密码等信息getDetails
:获取用户的额外信息getPrincipal
:获取用户身份信息,在未认证的情况下获取到的是用户名,已认证的情况下获取到的是UserDetails
isAuthenticated
:获取当前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 星期一
*/
@Component
public class UserAuthenticationProvider implements AuthenticationProvider {
private static final Log log = LogFactory.get(UserAuthenticationProvider.class);
@Autowired
private UserMapper userMapper;
@Override
public 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);
}
@Override
public 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
@RequiredArgsConstructor
public class JwtAuthTokenFilter extends OncePerRequestFilter {
final UserMapper userMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("进入了JwtAuthTokenFilter");
// 获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
filterChain.doFilter(request, response);
return;
}
// 解析token
Claims 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;
}
// 存储SecurityContextHolder
List<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)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
final JwtAuthTokenFilter jwtAuthTokenFilter;
final UserLogoutSuccessHandler userLogoutSuccessHandler;
final UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;
final UserAuthenticationProvider userAuthenticationProvider;
final UserAuthenticationEntryPoint userAuthenticationEntryPoint;
@Bean
public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler() {
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
handler.setPermissionEvaluator(new UserPermissionEvaluator());
return handler;
}
@Override
protected 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);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(userAuthenticationProvider);
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
测试
准备三个接口