实现JWT
工具类
将JWT
相关配置(密钥、过期时间等)封装为一个常量类:
package top.wteng.jwtsecurity;
public class SecurityConstants {
public static final String JWT_HEADER = "access-token";
public static final int EXPIRATION_MINUTES = 24;
public static final String ROLE_CLAIMS = "role";
public static final String TOKEN_PREFIX = "";
public static final String SECRET_KEY = "JUhs6j32^lderfewt3422*3243785fdfhght34u454trt,ldi823j3";
}
实现JWT
工具类如下:
package top.wteng.jwtsecurity.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import top.wteng.jwtsecurity.SecurityConstants;
import javax.crypto.SecretKey;
import javax.xml.bind.DatatypeConverter;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
public class JwtUtil {
private static final byte[] API_KEY_SECRET_BYTES = DatatypeConverter.parseBase64Binary(SecurityConstants.SECRET_KEY);
private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(API_KEY_SECRET_BYTES);
public static String createToken(String username, String id, List<String> roles) {
long expiration = SecurityConstants.EXPIRATION_MINUTES * 60;
final Date createdDate = new Date();
final Date expirationDate = new Date(createdDate.getTime() + expiration * 1000);
String tokenPrefix = Jwts.builder()
.setHeaderParam("type", "JWT")
.signWith(SECRET_KEY, SignatureAlgorithm.HS256)
.claim(SecurityConstants.ROLE_CLAIMS, String.join(",", roles))
.setId(id)
.setIssuer("SnailClimb")
.setIssuedAt(createdDate)
.setSubject(username)
.setExpiration(expirationDate)
.compact();
return SecurityConstants.TOKEN_PREFIX + tokenPrefix; // 添加 token 前缀 "Bearer ";
}
public static String getId(String token) {
Claims claims = getClaims(token);
return claims.getId();
}
public static UsernamePasswordAuthenticationToken getAuthentication(String token) {
Claims claims = getClaims(token);
List<SimpleGrantedAuthority> authorities = getAuthorities(claims);
String userName = claims.getSubject();
return new UsernamePasswordAuthenticationToken(userName, token, authorities);
}
private static List<SimpleGrantedAuthority> getAuthorities(Claims claims) {
// 注意传入角色信息时角色要以ROLE_开头,因为在校验时springscrurity会自动在角色名前加上前缀再比较
String role = (String) claims.get(SecurityConstants.ROLE_CLAIMS);
return Arrays.stream(role.split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
private static Claims getClaims(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
}
过滤器实现
package top.wteng.jwtsecurity.filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import io.jsonwebtoken.JwtException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import top.wteng.jwtsecurity.SecurityConstants;
import top.wteng.jwtsecurity.util.JwtUtil;
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private final Logger logger = LoggerFactory.getLogger(JwtAuthorizationFilter.class);
public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
private String getAccessToken(HttpServletRequest request) {
String token = request.getHeader(SecurityConstants.JWT_HEADER);
if (token == null) {
token = request.getParameter(SecurityConstants.JWT_HEADER);
}
return token;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = getAccessToken(request);
logger.info("Token Detected: {}", token);
if (token == null) {
SecurityContextHolder.clearContext();
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authenticationToken = null;
try {
authenticationToken = JwtUtil.getAuthentication(token);
} catch (JwtException e) {
logger.error("Invalid Token: {}", token);
SecurityContextHolder.clearContext();
chain.doFilter(request, response);
return;
}
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
chain.doFilter(request, response);
}
}
权限错误时的处理策略
有两种权限错误:
- 未携带或
token
非法(校验错误或过期) token
正确,但是没有足够的权限,如携带的是普通用户的token
,请求的却是只有管理员才能访问的接口,此时应抛出403
针对第一中情况,实现AuthenticationEntryPoint
实现一个进入点,返回异常提示:
package top.wteng.jwtsecurity.exception;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import top.wteng.jwtsecurity.util.ResultUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class UnAuthorizedExceptionEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper mapper = new ObjectMapper();
/**
* 当用户尝试访问需要权限才能的REST资源而不提供Token或者Token错误或者过期时,
* 将调用此方法发送401响应以及错误信息
*/
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setContentType("application/json;charset=utf8"); // 通过json格式返回
int statusCode = HttpServletResponse.SC_UNAUTHORIZED;
response.setStatus(statusCode);
PrintWriter writer = response.getWriter();
writer.write(mapper.writeValueAsString(ResultUtil.error(statusCode, "UnAuthorized")));
writer.flush();
writer.close();
}
}
针对第二种错误,实现AccessDeniedHandler
,抛出403
错误:
package top.wteng.jwtsecurity.exception;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import top.wteng.jwtsecurity.util.ResultUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper mapper = new ObjectMapper();
/**
* 当用户尝试访问需要权限才能的REST资源而权限不足的时候,
* 将调用此方法发送403响应以及错误信息
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
response.setContentType("application/json;charset=utf8");
int statusCode = HttpServletResponse.SC_FORBIDDEN;
response.setStatus(statusCode);
PrintWriter writer = response.getWriter();
writer.write(mapper.writeValueAsString(ResultUtil.error(statusCode, "No Enough Permissions")));
writer.flush();
writer.close();
}
}
配置
继承WebSecurityConfigurerAdapter
,实现相关配置,将过滤器、错误处理类等组合起来:
package top.wteng.jwtsecurity.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.Customizer;
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.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import top.wteng.jwtsecurity.SecurityConstants;
import top.wteng.jwtsecurity.exception.JwtAccessDeniedHandler;
import top.wteng.jwtsecurity.exception.UnAuthorizedExceptionEntryPoint;
import top.wteng.jwtsecurity.filter.JwtAuthorizationFilter;
import java.util.Arrays;
import java.util.Collections;
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors(Customizer.withDefaults())
.csrf().disable()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling().authenticationEntryPoint(new UnAuthorizedExceptionEntryPoint())
.accessDeniedHandler(new JwtAccessDeniedHandler());
http.headers().frameOptions().disable();
}
/**
* Cors配置
**/
@Bean
CorsConfigurationSource corsConfigurationSource() {
org.springframework.web.cors.CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Collections.singletonList("*"));
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "DELETE", "PUT", "OPTIONS"));
configuration.setExposedHeaders(Collections.singletonList(SecurityConstants.JWT_HEADER));
configuration.setAllowCredentials(false);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Controller
中使用
Controller
中在需要授权的接口上添加@PreAuthorize
注解即可:
@Controller
public class UserController {
@RequestMapping(value = "/user", method = RequestMethod.GET)
@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')")
@ResponseBody
public User getUser() {
// TOOD
}
}
springscurity
会自动在角色前加上ROLE_
前缀再与用户角色匹配,即即使写成@PreAuthorize('hasAnyRole('USER')')
,在匹配时也会变成ROLE_USER
去匹配,因此在设置SimpleGrantedAuthority
时,设置的角色名要带前缀。
获取用户凭据或认证信息
- 在普通
bean
中可以通过上下文获取:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication instanceof AnonymousAuthenticationToken)) {
String currentUserName = authentication.getName();
return currentUserName;
}
- 在
Controller
方法中可以直接注入Principal
或Authentication
:
@Controller
public class UserController {
@Autowired
private UserRep userRep;
@RequestMapping(value = "/user", method = RequestMethod.GET)
@ResponseBody
public User getUser(Principal principal, Authentication authentication) {
System.out.pringln("name = " + authentication.getName());
return this.userRep.findByUsername(principal.getName());
}
}
- 通过自定义接口获取:
public interface IAuthenticationFacade {
Authentication getAuthentication();
}
@Component
public class AuthenticationFacade implements IAuthenticationFacade {
@Override
public Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
}
在Controller
中注入:
@Controller
public class UserController {
@Autowired
private IAuthenticationFacade authenticationFacade;
@Autowired
private UserRep userRep;
@RequestMapping(value = "/user", method = RequestMethod.GET)
@ResponseBody
public User getUser() {
Authentication authentication = authenticationFacade.getAuthentication();
return this.userRep.findByUsername(authentication.getName());
}
}
自定义注解注入用户信息
虽然可以直接在Controller
方法中注入Principal
或Authentication
,但是只能获取到id
以及name
,想要获取到完整的User
对象,可以自定义参数注解,通过实现HandlerMethodArgumentResolver
参数解析器,根据name
查询数据库,将user
对象注入到方法参数里。
首先自定义一个注解:
package top.wteng.jwtsecurity.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthedUser {
}
然后实现HandlerMethodArgumentResolver
,自定义一个参数解析器,将@AuthedUser
注解标注的参数解析为User
对象进行注入,如下:
package top.wteng.jwtsecurity.resolver
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import top.wteng.jwtsecurity.annotation.AuthedUser;
import top.wteng.jwtsecurity.entity.User;
import top.wteng.jwtsecurity.repository.UserRep;
import java.lang.Exception;
@Component
public class AuthedUserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private UserRep userRep;
@Override
public Boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(AuthedUser.class) != null
}
@Override
public User resolveArgument(MethodParameter: parameter, ModelAndViewContainer: mavContainer, NativeWebRequest: webRequest, WebDataBinderFactory: binderFactory) {
Principal principal = webRequest.getUserPrincipal();
User user = null;
try {
user = userRep.findByUsername(principal.getName());
} catch (e: Exception) {
}
if (user == null) {
throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED, "user is invalid");
}
return user;
}
}
这样,就可以直接在Controller
方法中直接注入user
对象:
@Controller
public class UserController {
@RequestMapping(value = "/user", method = RequestMethod.GET)
@ResponseBody
public User getUser(@AuthedUser user) {
return user;
}
}