一、Spring Security基本原理
Spring Security在实现上是一系列过滤器,组成过滤器链,这些过滤器按一定的次序依次拦截请求,先是绿色的认证过滤器,再是蓝色的错误转换过滤器,再是橙色的安全拦截器,最后才是我们的接口。
Spring Security的身份认证,实际上是在其过滤器链的绿色区的某个节点上,根据一定的规则,构建一个认证Authentication,然后向SecurityContextHolder的当前上下文写入。
二、JWT令牌的身份认证过滤器
根据这个原理,我们编制一个过滤器,从jwt中解析出username和authorities,然后按照UsernamePasswordAuthenticationFilter中实现认证Authenticationde的方法,依次构造UserDetails、Authenticationde(UsernamePasswordAuthenticationToken),再把Authenticationde写入SecurityContextHolder即可。实现代码如下:
/**
*
*/
package com.jh.heroes.api.web.authentication;
import java.io.IOException;
import java.util.Collection;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import com.jh.heroes.api.exception.JwtAuthenticationException;
import com.jh.heroes.api.util.JwtHelper;
import io.jsonwebtoken.Claims;
/**
* jwt认证过滤器
*
* @author liangxh
*
*/
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter{
/*
* (non-Javadoc)
*
* @see
* org.springframework.web.filter.OncePerRequestFilter#doFilterInternal(javax.
* servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse,
* javax.servlet.FilterChain)
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException,JwtAuthenticationException {
if (SecurityContextHolder.getContext().getAuthentication() == null) {
try {
String authHeader = request.getHeader("Authorization");
String tokenHead = "Bearer ";
if (!StringUtils.isEmpty(authHeader) && authHeader.startsWith(tokenHead)) {
String token = authHeader.substring(tokenHead.length());
Claims claims = JwtHelper.parseJWT(token);
if (claims != null) {
String username = claims.get("username").toString();
String role = claims.get("role").toString();
String[] rolesArray = role.split(",");
Collection<? extends GrantedAuthority> roles = Stream.of(rolesArray)
.map(s->new SimpleGrantedAuthority(s)).collect(Collectors.toList());
// 1、构建UserDetails
UserDetails userDetails = new User(username, "N/A", roles);
// 2、构建已经认证的令牌
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, "N/A", userDetails.getAuthorities());
// 3、设置令牌的Details
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 4、把令牌写入到当前安全线程上下文中:告知Spring Security过滤器链,当前线程已经认证,无需再认证
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (JwtAuthenticationException exception) {
request.setAttribute("jwterror", exception.getMessage());
response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(), exception.getMessage());
return;
}
}
filterChain.doFilter(request, response);
}
}
三、重构认证成功和失败处理器
重构的目的是返回统一的数据结构,便于前台解析。
1、增加统一数据返回
package com.jh.heroes.api.util;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseMsg implements Serializable{
/**
* 序列化.
*/
private static final long serialVersionUID = -8739271187671909578L;
public static final String STATUS_SUCCESS = "0";
public static final String STATUS_FAILED = "-1";
/**
* 状态编码.
*/
private String status=STATUS_SUCCESS;
/**返回消息.*/
private String msg="";
/**返回数据.*/
private Object data;
/**
* 失败.
* @param msg 失败原因
*/
public void failed(String msg)
{
this.setStatus(STATUS_FAILED);
this.setMsg(msg);
}
}
2、增加异常类
/**
*
*/
package com.jh.heroes.api.exception;
import org.springframework.security.core.AuthenticationException;
/**
* JWT认证异常类.
* @author liangxh
*
*/
public class JwtAuthenticationException extends AuthenticationException {
/**
*
*/
private static final long serialVersionUID = 381802987164606243L;
/**消息编码*/
private String code = "";
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public JwtAuthenticationException(String msg) {
super(msg);
}
public JwtAuthenticationException(String code,String msg) {
super(msg);
this.code=code;
}
}
3、修改认证处理器
/**
*
*/
package com.jh.heroes.api.web.authentication;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jh.heroes.api.util.JwtHelper;
import com.jh.heroes.api.util.ResponseMsg;
import lombok.extern.slf4j.Slf4j;
/**
* 身份认证成功处理器
* @author liangxh
*
*/
@Slf4j
@Component("heroApiAuthenticationSuccessHandler")
public class HeroApiAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
/*
* (non-Javadoc)
*
* @see org.springframework.security.web.authentication.
* AuthenticationSuccessHandler#onAuthenticationSuccess(javax.servlet.http.
* HttpServletRequest, javax.servlet.http.HttpServletResponse,
* org.springframework.security.core.Authentication)
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
User user=(User)authentication.getPrincipal();
log.info("登录成功:username="+user.getUsername()+" RemoteAddr="+request.getRemoteAddr()+" RemoteHost="+request.getRemoteHost()+" RemotePort="+request.getRemotePort());
ResponseMsg msg=new ResponseMsg();
msg.setData(JwtHelper.createJWT(user));
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(msg));
}
}
4、修改认证失败处理器
/**
*
*/
package com.jh.heroes.api.web.authentication;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jh.heroes.api.util.ResponseMsg;
import lombok.extern.slf4j.Slf4j;
/**
* 身份认证失败处理器
*
* @author liangxh
*
*/
@Slf4j
@Component("heroApiAuthenctiationFailureHandler")
public class HeroApiAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
/*
* (non-Javadoc)
*
* @see
* org.springframework.security.web.authentication.AuthenticationFailureHandler#
* onAuthenticationFailure(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse,
* org.springframework.security.core.AuthenticationException)
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
log.info("登录失败:{}");
ResponseMsg msg=new ResponseMsg();
msg.failed(exception.getMessage());
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(msg));
}
}
四、增加访问无权限处理器
package com.jh.heroes.api.web.config;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jh.heroes.api.util.ResponseMsg;
import lombok.extern.slf4j.Slf4j;
/**
* 自定了权限不足的返回值
* @author liangxh
*
*/
@Slf4j
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.error(accessDeniedException.getMessage());
ResponseMsg msg=new ResponseMsg();
msg.failed("权限不足,不能访问");
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(msg));
}
}
五、增加未登录或认证不成功处理器
package com.jh.heroes.api.web.config;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jh.heroes.api.util.ResponseMsg;
import lombok.extern.slf4j.Slf4j;
/**
* 未登录或认证不成功处理器
* @author liangxh
* 我们没有使用form或basic等验证机制,需要自定义一个AuthenticationEntryPoint,当未验证用户访问受限资源时,返回401错误。如没有自定义AuthenticationEntryPoint,将返回403错误。使用方法见WebSecurityConfig。
*/
@Slf4j
@Component
public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {
@Autowired
private ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// This is invoked when user tries to access a secured REST resource without supplying any credentials
// We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
log.info(authException.getMessage());
ResponseMsg msg=new ResponseMsg();
Object jwtErr=request.getAttribute("jwterror");
if (jwtErr!=null) {
msg.failed(jwtErr.toString());
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
} else {
msg.failed("请登录");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(msg));
}
}
注:在这个处理器中,判断是否存在jwterror(这个键取决于JWT令牌的身份认证过滤器),存在则说明在JWT令牌的身份认证过滤器中存在令牌错误,从而身份认证失败的。
六、修改WebSecurityConfig
package com.jh.heroes.api.web.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.jh.heroes.api.web.authentication.JsonUsernamePasswordAuthenticationFilter;
import com.jh.heroes.api.web.authentication.JwtAuthenticationTokenFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 未登录或认证不成功处理器
*/
@Autowired
private EntryPointUnauthorizedHandler unauthorizedHandler;
/**
* 访问无权限处理器
*/
@Autowired
private AccessDeniedHandler accessDeniedHandler;
/**
* 密码编码、解码器
*/
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 用户
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 登录成功处理器
*/
@Autowired
private AuthenticationSuccessHandler heroApiAuthenticationSuccessHandler;
/**
* 登录失败处理器
*/
@Autowired
private AuthenticationFailureHandler heroApiAuthenctiationFailureHandler;
/* (non-Javadoc)
* @see org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#authenticationManagerBean()
*/
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
//解决 http.getSharedObject(AuthenticationManager.class) 无法获取AuthenticationManager实例
return super.authenticationManagerBean();
}
@Autowired
protected AuthenticationManager authenticationManager;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter=new JsonUsernamePasswordAuthenticationFilter();
jsonUsernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager);
jsonUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(heroApiAuthenticationSuccessHandler);
jsonUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(heroApiAuthenctiationFailureHandler);
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter=new JwtAuthenticationTokenFilter();
http
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(jsonUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.cors()
.and()
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).accessDeniedHandler(accessDeniedHandler)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // don't create session
.and()
.authorizeRequests()
.antMatchers("/jsonlogin").permitAll()
.antMatchers("/login").permitAll()
.anyRequest()
.authenticated();
}
}
这次的配置文件主要存在一下修改:
1、取消formlogin认证模式,避免formLogin认证中的页面跳转等等。
2、取消session支持。
3、注入认证不成功和访问权限不足处理器。
4、把jwtAuthenticationTokenFilter注入到UsernamePasswordAuthenticationFilter的实例之前。
七、小结
1、充分利用Spring Security基本原理,通过过滤器解析jwt实现身份模拟认证;并且通过jwt实现自认证,不访问数据库。
2、取消formlogin认证模式,增加认证不成功和访问权限不足处理器,便于前后端分离模式。
3、统一数据返回,便于前台解析数据。
八、参考
1、JWT+Redis+Spring Security 实现无状态化认证
2、Spring Security + JWT 实现基于Token的安全验证
3、Spring Boot实战之Filter实现使用JWT进行接口认证