此文章用到的版本
spring-boot : 2.6.8
java 1.8
引入依赖包(gradle) maven 请自行转换
dependencies {
compile group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
}
先说说原理
UsernamePasswordAuthenticationFilter 继承 AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter.doFilter() 会执行 抽象方法attemptAuthentication ()
通过观察发现 UsernamePasswordAuthenticationFilter 会拦截 POST /login 的请求
然后通过会通过Http parameter 获取 username 和 password 参数的值执行鉴权认证
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
"POST");
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
成功会执行 SavedRequestAwareAuthenticationSuccessHandler 重定向到指定url
public class SavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private RequestCache requestCache = new HttpSessionRequestCache();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
return;
}
String targetUrlParameter = getTargetUrlParameter();
if (isAlwaysUseDefaultTargetUrl()
|| (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
this.requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
return;
}
clearAuthenticationAttributes(request);
// Use the DefaultSavedRequest URL
String targetUrl = savedRequest.getRedirectUrl();
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
public void setRequestCache(RequestCache requestCache) {
this.requestCache = requestCache;
}
}
失败会执行 SimpleUrlAuthenticationFailureHandler
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (this.defaultFailureUrl == null) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Sending 401 Unauthorized error since no failure URL is set");
}
else {
this.logger.debug("Sending 401 Unauthorized error");
}
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
return;
}
saveException(request, exception);
if (this.forwardToDestination) {
this.logger.debug("Forwarding to " + this.defaultFailureUrl);
request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);
}
else {
this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);
}
}
这种方式不兼容json方式提交的登录 而且不能返回token 供前端使用 所以我们需要改造此Filter
实现思路:
1. 拦截Post /login 请求
2. 获取请求中的body参数 username 以及password
3. 返回 UsernamePasswordAuthenticationFilter 不携带权限集合
4. 重写 UserDetailsService 查询数据库
5. 重写 AuthenticationSuccessHandler 登录成功后返回jwt token令牌
6. 重写 AuthenticationFailureHandler 失败返回失败原因 例如:密码错误,账户锁定, 账户不存在
定义token常量
public class SecurityConstants
{
public static final long EXPIRATION_TIME = 864_000_000; // 10 days
public static final String TOKEN_PREFIX = "Bearer ";
public static final String HEADER_STRING = "Authorization";
private SecurityConstants()
{
throw new IllegalStateException("Utility class");
}
}
实现工具类 JWTUtils 用户生成、解析token
@Component
public class JwtUtil
{
/**
* 签名用的密钥
*/
private static String signKey = "nacl";
/**
* 用户登录成功后生成Jwt
* 使用Hs256算法
*
* @param exp jwt过期时间
* @param claims 保存在Payload(有效载荷)中的内容
* @return token字符串
*/
public static String createJWT(Date exp, Map<String, Object> claims)
{
//指定签名的时候使用的签名算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成JWT的时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//创建一个JwtBuilder,设置jwt的body
JwtBuilder builder = Jwts.builder()
//保存在Payload(有效载荷)中的内容
.setClaims(claims)
//iat: jwt的签发时间
.setIssuedAt(now)
//设置过期时间
.setExpiration(exp)
//设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, signKey);
return builder.compact();
}
/**
* 解析token,获取到Payload(有效载荷)中的内容,包括验证签名,判断是否过期
*
* @param token
* @return
*/
public static Claims parseJWT(String token)
{
//得到DefaultJwtParser
return Jwts.parser()
//设置签名的秘钥
.setSigningKey(signKey)
//设置需要解析的token
.parseClaimsJws(token).getBody();
}
}
开始实现身份认证过滤器(JWTAuthenticationFilter) 继承 UsernamePasswordAuthenticationFilter 重写 attemptAuthentication 方法
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter
{
@Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res)
{
Map<String, String> creds = new HashMap<>();
try
{
creds = new ObjectMapper().readValue(req.getInputStream(), Map.class); // 获取body中的参数
} catch (IOException e)
{
e.printStackTrace();
}
return this.getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
creds.get("username"),
creds.get("password"),
new ArrayList<>())
);
}
}
重写UserDetailService
返回一个测试用户 用户名:123 密码:123 角色权限: ROLE_ADMIN
@Service
public class UserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService
{
@Override
public UserDetails loadUserByUsername(String username)
{
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); // 设定权限
return new User(
"123", // username
new BCryptPasswordEncoder().encode("123") , // password
true, // enabled – set to true if the user is enabled
true, // accountNonExpired – set to true if the account has not expired
true, // credentialsNonExpired – set to true if the credentials have not expired
true, // accountNonLocked – set to true if the account is not locked
authorities // authorities – the authorities that should be granted to the caller if they presented the
);
}
}
重写 AuthenticationSuccessHandler 成功后将生成的 token 放入 response header
@Component
public class CustomAuthenticateSuccessHandler implements AuthenticationSuccessHandler
{
@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication auth) throws IOException, ServletException
{
Map<String, Object> claims = new HashMap<>();
claims.put("username", ((User) auth.getPrincipal()).getUsername());
String token = JwtUtil.createJWT(
new Date(System.currentTimeMillis() + SecurityConstants.EXPIRATION_TIME),
claims
);
response.addHeader(SecurityConstants.HEADER_STRING, SecurityConstants.TOKEN_PREFIX + token);
}
}
重写 AuthenticationFailureHandler 返回账户失败原因
@Component
public class CustomAuthenticateFailureHandler implements AuthenticationFailureHandler
{
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException failed
) throws IOException, ServletException
{
String returnData = "";
// 账号过期
if (failed instanceof AccountExpiredException)
{
returnData = "账号过期";
}
// 密码错误
else if (failed instanceof BadCredentialsException)
{
returnData = "密码错误";
}
// 密码过期
else if (failed instanceof CredentialsExpiredException)
{
returnData = "密码过期";
}
// 账号不可用
else if (failed instanceof DisabledException)
{
returnData = "账号不可用";
}
//账号锁定
else if (failed instanceof LockedException)
{
returnData = "账号锁定";
}
// 用户不存在
else if (failed instanceof InternalAuthenticationServiceException)
{
returnData = "用户不存在";
}
// 其他错误
else
{
returnData = "未知异常";
}
// 处理编码方式 防止中文乱码
response.setContentType("text/json;charset=utf-8");
// 将反馈塞到HttpServletResponse中返回给前台
response.getWriter().write(returnData);
}
}
改造BasicAuthenticationFilter基于JWT解析 实现权限认证
认证过滤器 BasicAuthenticationFilter
header里头有Authorization
,而且value是以Basic
开头的,则走BasicAuthenticationFilter
,提取参数构造UsernamePasswordAuthenticationToken
进行认证,成功则填充SecurityContextHolder的Authentication
而我们要做的是 header里头有Authorization
,而且value是以Bearer
开头的, 解析jwt token填充SecurityContextHolder的Authentication
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request);
if (authRequest == null) {
this.logger.trace("Did not process authentication request since failed to find "
+ "username and password in Basic Authorization header");
chain.doFilter(request, response);
return;
}
String username = authRequest.getName();
this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username));
if (authenticationIsRequired(username)) {
Authentication authResult = this.authenticationManager.authenticate(authRequest);
SecurityContextHolder.getContext().setAuthentication(authResult);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);
}
}
catch (AuthenticationException ex) {
SecurityContextHolder.clearContext();
this.logger.debug("Failed to process authentication request", ex);
this.rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, ex);
if (this.ignoreFailure) {
chain.doFilter(request, response);
}
else {
this.authenticationEntryPoint.commence(request, response, ex);
}
return;
}
chain.doFilter(request, response);
}
@Override
public UsernamePasswordAuthenticationToken convert(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null) {
return null;
}
header = header.trim();
if (!StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BASIC)) {
return null;
}
if (header.equalsIgnoreCase(AUTHENTICATION_SCHEME_BASIC)) {
throw new BadCredentialsException("Empty basic authentication token");
}
byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
byte[] decoded = decode(base64Token);
String token = new String(decoded, getCredentialsCharset(request));
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(token.substring(0, delim),
token.substring(delim + 1));
result.setDetails(this.authenticationDetailsSource.buildDetails(request));
return result;
}
开始实现 继承BasicAuthenticationFilter 并重写 doFilterInternal 方法
getAuthentication 获取 request header中的token 解析成username 并调用Userservice中的loadUserByName方法返回User鉴权信息
装入SecurityContextHolder
public class JWTAuthorizationFilter extends BasicAuthenticationFilter
{
public JWTAuthorizationFilter(AuthenticationManager authManager)
{
super(authManager);
}
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException
{
String header = req.getHeader(SecurityConstants.HEADER_STRING);
if (header == null || !header.startsWith(SecurityConstants.TOKEN_PREFIX))
{
chain.doFilter(req, res);
return;
}
try
{
UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, res);
} catch (ExpiredJwtException e)
{
res.getWriter().write("token expired");
} catch (JwtException e)
{
res.getWriter().write("token invalid");
}
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request)
{
String token = request.getHeader(SecurityConstants.HEADER_STRING);
if (token != null)
{
// parse the token.
Claims claims = JwtUtil.parseJWT(token.replace(SecurityConstants.TOKEN_PREFIX, ""));
if (claims != null)
{
UserDetailsService userDetailsService = new UserDetailsService();
User u = (User) userDetailsService.loadUserByUsername((String) claims.get("username"));
return new UsernamePasswordAuthenticationToken(u.getUsername(), u.getPassword(), u.getAuthorities());
}
}
return null;
}
}
CustomAccessDeniedHandler 非匿名下的错误拦截器
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler
{
@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException
{
response.setContentType("text/json;charset=utf-8");
response.getWriter().write("权限错误");
}
}
CustomAuthenticationEntryPoint 匿名下的错误拦截器
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint
{
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException
{
response.getWriter().write("no login");
}
}
OK 准备大功告成, 最后设定一下Spring Security的配置
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter
{
private final UserDetailsService userDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final CustomAuthenticateSuccessHandler customAuthenticateSuccessHandler;
private final CustomAuthenticateFailureHandler customAuthenticateFailureHandler;
public SpringSecurityConfig(
UserDetailsService userDetailsService,
BCryptPasswordEncoder bCryptPasswordEncoder,
CustomAccessDeniedHandler customAccessDeniedHandler,
CustomAuthenticationEntryPoint customAuthenticationEntryPoint,
CustomAuthenticateSuccessHandler customAuthenticateSuccessHandler,
CustomAuthenticateFailureHandler customAuthenticateFailureHandler
)
{
this.userDetailsService = userDetailsService;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
this.customAccessDeniedHandler = customAccessDeniedHandler;
this.customAuthenticationEntryPoint = customAuthenticationEntryPoint;
this.customAuthenticateFailureHandler = customAuthenticateFailureHandler;
this.customAuthenticateSuccessHandler = customAuthenticateSuccessHandler;
}
@Override
public void configure(HttpSecurity http) throws Exception
{
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().csrf().disable()
.authorizeRequests().antMatchers("/sign-up").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.accessDeniedHandler(customAccessDeniedHandler)
.authenticationEntryPoint(customAuthenticationEntryPoint)
.and()
.addFilter(jwtAuthenticationFilter())
.addFilter(jwtAuthorizationFilter());
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
@Bean
public JWTAuthenticationFilter jwtAuthenticationFilter() throws Exception
{
JWTAuthenticationFilter filter = new JWTAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(customAuthenticateSuccessHandler);
filter.setAuthenticationFailureHandler(customAuthenticateFailureHandler);
return filter;
}
@Bean
public JWTAuthorizationFilter jwtAuthorizationFilter () throws Exception
{
return new JWTAuthorizationFilter(authenticationManager());
}
}
编写测试Controller
@RestController
public class TestController
{
@PostMapping("/sign-up")
public String signUp ()
{
return "1111";
}
@PostMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String admin ()
{
return "222";
}
@PostMapping("/user")
@PreAuthorize("hasRole('USER')")
public String user ()
{
return "333";
}
}
测试 /login 登录
输入错误的账号密码
输入正确的账号密码
放行请求/sign-up
身份鉴权认证
无token
错误token
正确token 无权限
正确token 有权限