功能
将JWT整合到项目中,具体分2个阶段
1.首次登录进行认证,认证成功就返回token
2.之后的请求携带token进行授权认证,就是说每次授权前都需要重新认证了
1 github: 源码地址
2 security08 子工程
引入jwt依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.yzm</groupId>
<artifactId>security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>security08</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>security08</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>com.yzm</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
项目结构
application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test04?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: root
password: root
main:
allow-bean-definition-overriding: true
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml
type-aliases-package: com.yzm.security08.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3 JwtUtils jwt工具类
package com.yzm.security08.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.codec.binary.Base64;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* JWT工具类
*/
public class JwtUtils implements Serializable {
private static final long serialVersionUID = 8527289053988618229L;
/**
* token头
* token前缀
*/
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Basic ";
/**
* 用户名称
*/
public static final String USERNAME = Claims.SUBJECT;
public static final String PASSWORD = "password";
/**
* 权限列表
*/
public static final String AUTHORITIES = "authorities";
/**
* 密钥
*/
private static final String SECRET = "abcdefg";
private static final String JWT_SECRET = "7786df7fc3a34e26a61c034d5ec8245d";
/**
* 过期时间5分钟
* 刷新时间2分钟
*/
public static final long TOKEN_EXPIRED_TIME = 5 * 60 * 1000L;
public static final long TOKEN_REFRESH_TIME = 2 * 60 * 1000L;
public static String generateToken(Map<String, Object> claims) {
return generateToken(claims, 0L);
}
/**
* 生成令牌
*/
public static String generateToken(Map<String, Object> claims, long expireTime) {
if (expireTime <= 0L) expireTime = TOKEN_EXPIRED_TIME;
Map<String, Object> headMap = new HashMap<>();
headMap.put("typ", "JWT");
headMap.put("alg", "HS256");
return Jwts.builder()
.setHeader(headMap)
//JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击
.setId(UUID.randomUUID().toString())
//.setIssuer("该JWT的签发者,是否使用是可选的")
//.setSubject("该JWT所面向的用户,是否使用是可选的")
//.setAudience("接收该JWT的一方,是否使用是可选的")
//如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
//签发时间(token生成时间)
.setIssuedAt(new Date())
//生效时间(在指定时间之前令牌是无效的)
.setNotBefore(new Date())
//过期时间(在指定时间之后令牌是无效的)
.setExpiration(new Date(System.currentTimeMillis() + expireTime))
//设置签名使用的签名算法和签名使用的秘钥
// .signWith(SignatureAlgorithm.HS256, JWT_SECRET)
.signWith(SignatureAlgorithm.HS256, generalKey())
.compact();
}
/**
* 验证令牌
*/
public static Claims verifyToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
//签名秘钥
// .setSigningKey(JWT_SECRET)
.setSigningKey(generalKey())
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
// token过期是直接抛出异常的,但仍然可以获取到claims对象
claims = e.getClaims();
}
return claims;
}
/**
* 由密钥生成加密key
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.decodeBase64(SECRET);
// return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return new SecretKeySpec(encodedKey, SignatureAlgorithm.HS256.getJcaName());
}
/**
* 是否过期
* true:过期
* false:未过期
*/
public static boolean isExpired(String token) {
Claims claims = verifyToken(token);
//和当前时间进行对比来判断是否过期
return claims.getExpiration().before(new Date());
}
/**
* 从令牌中获取用户名
*/
public static String getUsernameFromToken(String token) {
Claims claims = verifyToken(token);
return claims.getSubject();
}
/**
* 获取请求token
*/
public static String getTokenFromRequest(HttpServletRequest request) {
String token = request.getHeader(TOKEN_HEADER);
if (token == null) token = request.getHeader("token");
if (StringUtils.isBlank(token))
return null;
return token.startsWith(TOKEN_PREFIX) ? token.substring(TOKEN_PREFIX.length()) : token;
}
}
4 第一阶段,登录认证,获取token
首次登录,还是以传统的表单登录模式,所以我们可以直接使用UsernamePasswordAuthenticationFilter来拦截/login进行认证处理,不过我这里自定义了一个JwtAuthenticationFilter,其功能跟UsernamePasswordAuthenticationFilter差不多
JwtAuthenticationFilter 继承 UsernamePasswordAuthenticationFilter 并重写 attemptAuthentication
package com.yzm.security08.config;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public JwtAuthenticationFilter() {
super();
}
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username = obtainUsername(request);
String password = obtainPassword(request);
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
throw new AuthenticationServiceException("用户名密码错误");
}
// 构造未鉴权的JwtAuthenticationToken 对象
JwtAuthenticationToken authToken = new JwtAuthenticationToken(username, password);
this.setDetails(request, authToken);
return this.getAuthenticationManager().authenticate(authToken);
}
}
JwtAuthenticationToken 继承 UsernamePasswordAuthenticationToken
package com.yzm.security08.config;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken {
public JwtAuthenticationToken(Object principal, Object credentials) {
super(principal, credentials);
}
public JwtAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
身份认证 Provider,获取UserDetails对象,校验密码,构造鉴权的Authentication对象
package com.yzm.security08.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
@Slf4j
public class JwtAuthenticationProvider implements AuthenticationProvider {
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
public JwtAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
@Override
public boolean supports(Class<?> authentication) {
return JwtAuthenticationToken.class.isAssignableFrom(authentication);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
JwtAuthenticationToken authenticationToken = (JwtAuthenticationToken) authentication;
String username = (String) authenticationToken.getPrincipal();
String password = (String) authenticationToken.getCredentials();
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails == null) throw new UsernameNotFoundException("账号异常");
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("凭证异常");
}
// 构造鉴权的JwtAuthenticationToken对象
JwtAuthenticationToken resultToken = new JwtAuthenticationToken(username, password,
userDetails.getAuthorities());
resultToken.setDetails(authentication.getDetails());
return resultToken;
}
}
登录认证成功之后,返回token
package com.yzm.security08.config;
import com.yzm.common.utils.HttpUtils;
import com.yzm.security08.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class JwtAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
if (authentication instanceof JwtAuthenticationToken) {
log.info("登录认证");
JwtAuthenticationToken authenticationToken = (JwtAuthenticationToken) authentication;
String username = (String) authenticationToken.getPrincipal();
Map<String, Object> map = new HashMap<>();
map.put(JwtUtils.USERNAME, username);
String token = JwtUtils.generateToken(map);
HttpUtils.successWrite(response, token);
}
}
}
5 第二阶段,携带token,授权认证
在上面第一阶段,获取到token,那么在之后的请求头中就携带token访问,所以我们需要定义一个Filter来拦截请求头中有 Authorization参数的请求url
JwtAuthorizationFilter 授权过滤器,仿写 AbstractAuthenticationProcessingFilter的
package com.yzm.security08.config;
import com.yzm.security08.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.*;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
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;
public class JwtAuthorizationFilter extends OncePerRequestFilter {
// 拦截header中带Authorization的请求
private final RequestMatcher authorizationRequestMatcher = new RequestHeaderRequestMatcher("Authorization");
private final AuthenticationManager authenticationManager;
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
protected boolean requiresAuthentication(HttpServletRequest request) {
return authorizationRequestMatcher.matches(request);
}
@Override
public void afterPropertiesSet() {
Assert.notNull(authenticationManager, "authenticationManager must be specified");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// 请求头是否携带 Authorization
if (!requiresAuthentication(request)) {
// 请求头没携带Authorization,可能是登录或匿名访问,直接放行,继续下一个过滤链
chain.doFilter(request, response);
return;
}
Authentication authResult = null;
AuthenticationException failed = null;
try {
String token = JwtUtils.getTokenFromRequest(request);
if (StringUtils.isNotBlank(token)) {
// 构造未鉴权的JwtAuthorizationToken,认证主体是token,传递给 Provider
JwtAuthorizationToken authorizationToken = new JwtAuthorizationToken(token, null);
authorizationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
authResult = this.authenticationManager.authenticate(authorizationToken);
} else {
failed = new InsufficientAuthenticationException("JWT is Empty");
}
} catch (AuthenticationException e) {
SecurityContextHolder.clearContext();
failed = e;
}
if (authResult != null) {
this.successfulAuthentication(request, response, authResult);
} else {
// 认证失败
this.unsuccessfulAuthentication(request, response, failed);
return;
}
// 放行,继续下一个过滤链
chain.doFilter(request, response);
}
protected void unsuccessfulAuthentication(
HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
// 认证失败,清除
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, failed);
}
protected void successfulAuthentication(
HttpServletRequest request, HttpServletResponse response, Authentication authResult)
throws IOException, ServletException {
// 认证成功,保存 Authentication 对象到上下文
SecurityContextHolder.getContext().setAuthentication(authResult);
successHandler.onAuthenticationSuccess(request, response, authResult);
}
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) {
Assert.notNull(successHandler, "successHandler cannot be null");
this.successHandler = successHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) {
Assert.notNull(failureHandler, "failureHandler cannot be null");
this.failureHandler = failureHandler;
}
protected AuthenticationSuccessHandler getSuccessHandler() {
return this.successHandler;
}
protected AuthenticationFailureHandler getFailureHandler() {
return this.failureHandler;
}
}
JwtAuthorizationToken 继承 UsernamePasswordAuthenticationToken
package com.yzm.security08.config;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class JwtAuthorizationToken extends UsernamePasswordAuthenticationToken {
public JwtAuthorizationToken(Object principal, Object credentials) {
super(principal, credentials);
}
public JwtAuthorizationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
同样的需要身份验证 Provider,就自定义实现了
package com.yzm.security08.config;
import com.yzm.security08.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.www.NonceExpiredException;
public class JwtAuthorizationProvider implements AuthenticationProvider {
private final UserDetailsService userDetailsService;
public JwtAuthorizationProvider(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
public boolean supports(Class<?> authentication) {
return JwtAuthorizationToken.class.isAssignableFrom(authentication);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
JwtAuthorizationToken authorizationToken = (JwtAuthorizationToken) authentication;
String token = (String) authorizationToken.getPrincipal();
if (JwtUtils.isExpired(token)) throw new NonceExpiredException("Token is Expired");
Claims claims = JwtUtils.verifyToken(token);
String username = claims.getSubject();
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails == null) throw new UsernameNotFoundException("账号异常");
// 返回鉴权Authentication对象
return new JwtAuthorizationToken(token, null, userDetails.getAuthorities());
}
}
返回鉴权的Authentication对象给JwtAuthorizationFilter,JwtAuthorizationFilter调用成功后处理,存储Authentication对象到上下文中
授权认证成功之后,调用成功处理程序,检查是否需要刷新token,并返回到响应头
package com.yzm.security08.config;
import com.yzm.security08.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class JwtAuthorizationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
if (authentication instanceof JwtAuthorizationToken) {
log.info("授权认证");
JwtAuthorizationToken authorizationToken = (JwtAuthorizationToken) authentication;
String token = (String) authorizationToken.getPrincipal();
Claims claims = JwtUtils.verifyToken(token);
long expiration = claims.getExpiration().getTime();
// 当前时间加上刷新时间大于过期时间,则刷新token
if (System.currentTimeMillis() + JwtUtils.TOKEN_REFRESH_TIME >= expiration) {
String username = claims.getSubject();
Map<String, Object> map = new HashMap<>();
map.put(JwtUtils.USERNAME, username);
token = JwtUtils.generateToken(map);
}
response.setHeader("Authorization", token);
}
}
}
6 一二阶段完成之后,需要配置到Security框架中
package com.yzm.security08.config;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* 登录认证
**/
public class JwtAuthenticationConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
public JwtAuthenticationConfigurer(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
@Override
public void configure(HttpSecurity http) throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(http.getSharedObject(AuthenticationManager.class));
jwtAuthenticationFilter.setAuthenticationSuccessHandler(new JwtAuthenticationSuccessHandler());
jwtAuthenticationFilter.setAuthenticationFailureHandler(new JwtFailureHandler());
JwtAuthenticationProvider jwtAuthenticationProvider = new JwtAuthenticationProvider(userDetailsService, passwordEncoder);
http
.addFilterAt(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.authenticationProvider(jwtAuthenticationProvider);
}
}
package com.yzm.security08.config;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
/**
* 授权认证
**/
public class JwtAuthorizationConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final UserDetailsService userDetailsService;
public JwtAuthorizationConfigurer(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
public void configure(HttpSecurity http) throws Exception {
JwtAuthorizationFilter jwtAuthorizationFilter = new JwtAuthorizationFilter(http.getSharedObject(AuthenticationManager.class));
jwtAuthorizationFilter.setAuthenticationSuccessHandler(new JwtAuthorizationSuccessHandler());
jwtAuthorizationFilter.setAuthenticationFailureHandler(new JwtFailureHandler());
JwtAuthorizationProvider jwtAuthorizationProvider = new JwtAuthorizationProvider(userDetailsService);
http
.addFilterAfter(jwtAuthorizationFilter, JwtAuthenticationFilter.class)
.authenticationProvider(jwtAuthorizationProvider);
}
}
在SecurityConfig全局配置中使用
既然我们使用了jwt,那么就禁用掉session
也不是页面的表单登录,使用postman请求/login进行登录,所以放行/login
注释配置用户,不使用默认UsernamePasswordAuthenticationFilter,用自定义的
package com.yzm.security08.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
public SecurityConfig(@Qualifier("jwtUserDetailsServiceImpl") UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
/**
* 密码编码器
* passwordEncoder.encode是用来加密的,passwordEncoder.matches是用来解密的
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// /**
// * 配置用户
// * 指定默认从哪里获取认证用户的信息,即指定一个UserDetailsService接口的实现类
// */
// @Override
// protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// // 从数据库读取用户、并使用密码编码器解密
// auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
// }
//配置资源权限规则
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // 关闭CSRF
.sessionManagement().disable() //禁用session
.formLogin().disable() //禁用form登录
// 添加token认证
.apply(new JwtAuthenticationConfigurer(userDetailsService, passwordEncoder()))
.and()
// 添加token授权
.apply(new JwtAuthorizationConfigurer(userDetailsService))
.and()
.exceptionHandling()
.accessDeniedHandler(new JwtAccessDeniedHandler())
.and()
// 退出登录
.logout()
.permitAll()
.and()
// 访问路径URL的授权策略,如注册、登录免登录认证等
.authorizeRequests()
.antMatchers("/", "/home", "/register", "/login").permitAll() //指定url放行
.anyRequest().authenticated() //其他任何请求都需要身份认证
;
}
}
一二阶段的认证失败都是使用同一处理程序
package com.yzm.security08.config;
import com.yzm.common.utils.HttpUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class JwtFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("登录失败:" + exception.getMessage());
String msg = "登录失败";
if (exception instanceof BadCredentialsException || exception instanceof UsernameNotFoundException) {
msg = "用户名或密码错误";
} else if (exception instanceof LockedException) {
msg = "账号被锁定";
} else if (exception instanceof AccountExpiredException) {
msg = "账号过期";
} else if (exception instanceof CredentialsExpiredException) {
msg = "凭证过期";
} else if (exception instanceof DisabledException) {
msg = "账号被禁用";
}
HttpUtils.errorWrite(response, msg);
}
}
无权访问处理程序
package com.yzm.security08.config;
import com.yzm.common.utils.HttpUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 认证过的用户访问无权限资源时的异常
*/
@Slf4j
public class JwtAccessDeniedHandler extends AccessDeniedHandlerImpl {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.info("授权失败:" + accessDeniedException.getMessage());
HttpUtils.errorWrite(response, accessDeniedException.getMessage());
}
}
到这里就基本配置完成了,下面进行测试
7 测试
首次登录,进行认证,获取token
携带token,访问