一、spring boot Security+JWT 在Spring Cloud网关层实现用户认证
1、引入Spring Security
org.springframework.boot
spring-boot-starter-security
2、创建WebSecurityConfig继承WebSecurityConfigurerAdapter
重写configure(HttpSecurity http)方法。WebSecurityConfigurerAdapter是由Spring Security提供的Web应用安全配置的适配器。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 由于使用的是JWT,我们这里不需要csrf
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// 对于获取token的rest api要允许匿名访问
.antMatchers("/api-user/safeVerify/**").permitAll()
.antMatchers("/api-base/manage/userLogin/**").permitAll()
.antMatchers("/api-user/app/login").permitAll()
.antMatchers("/api-user/app/version").permitAll()
.antMatchers("/api-user/app/refresh/token").permitAll()
.antMatchers("/swagger-ui.html/**", "/swagger-resources/**", "/*/v2/api-docs/**").permitAll()//swagger文档无授权访问
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
// 禁用缓存
httpSecurity.headers().cacheControl();
}
}
Spring Security包含了众多的过滤器,这些过滤器形成了一条链,所有请求都必须通过这些过滤器后才能成功访问到资源。其中UsernamePasswordAuthenticationFilter过滤器用于处理基于表单方式的登录认证。我们通过该过滤器,实现JWT的用户认证。
3、创建JWTAuthenticationTokenFilter 过滤器,实现对请求token的用户认证
@SuppressWarnings("SpringJavaAutowiringInspection")
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private RedisUtil redisUtil;
@Autowired
private UserApi userApi;
private static final String ERP_HEADER = "authorization-erp-fqkj";
private static final String FILTER_APPLIED = "__spring_security_Filter_filterApplied";
private static final String HEADER_USER = "key_userinfo_in_http_header";
private static final String TOKEN_EXPRIED = "Filter_TokenExpried";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
if (request.getMethod().equals("OPTIONS")) {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Content-Type", "application/json");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with,Content-Type,authorization-erp-fqkj,authorization-app-fqkj,authorization-manage-fqkj");
response.setStatus(HttpServletResponse.SC_OK);
return;
}
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, true);
AppendHeaderRequestWrapper requestWrapper = new AppendHeaderRequestWrapper(request);
String authToken = request.getHeader(ERP_HEADER);
if (authToken != null) {
String userSubject = jwtTokenUtil.getUserIdFromToken(authToken);
boolean isExpired = jwtTokenUtil.isTokenExpired(authToken);
if(!isExpired){
setUserSecurityContext(userSubject, requestWrapper);
}
}
chain.doFilter(requestWrapper, response);
}
private void setUserSecurityContext(String userSubject, AppendHeaderRequestWrapper requestWrapper) {
String companyId = userSubject.split("#")[0];
String userId = userSubject.split("#")[1];
String key = companyId + RedisSuffixConstants.LOGIN_USERLIST;
boolean isExist = redisUtil.hHasKey(key, userId);
UserInfo userInfo;
if (isExist) {
userInfo = redisUtil.hget(key, userId);
} else {
//如果redis中不存在
userInfo = userApi.getUserInfoByUserId(userId);
}
if (userInfo == null) {
return;
}
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userSubject);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(requestWrapper));
SecurityContextHolder.getContext().setAuthentication(authentication);
redisUtil.hset(key, userId, userInfo);
UserInfoContext.setUser(userInfo);
String userJson = JSON.toJSONString(userInfo);
try {
requestWrapper.putHeader(HEADER_USER, URLDecoder.decode(userJson, "UTF-8"));
} catch (UnsupportedEncodingException e) {
log.error("init userInfo error", e);
}
}
}
该过滤器的处理步骤:
1)判断httpmothod为“OPTIONS”,直接放通,允许跨域访问。
2)判断请求header中是否存在指定名的token,调用JWT工具类获取当前用户标识,判断token是否过期。
3)根据用户标识从redis中获取当前用户的基本信息,没有调用会员服务获取用户信息。并更新redis。
4) 生成UsernamePasswordAuthenticationToken,保存到SecurityContext中。
5)把用户基本信息ToJson为文本,保存到请求头中。
4、包装当前请求类HttpServletRequestWrapper,在当前请求头中加入登录用户的基本信息
public class AppendHeaderRequestWrapper extends HttpServletRequestWrapper {
private final Map customHeaders;
public AppendHeaderRequestWrapper(HttpServletRequest request) {
super(request);
this.customHeaders = new HashMap<>();
}
void putHeader(String name, String value){
this.customHeaders.put(name, value);
}
@Override
public String getHeader(String name) {
// check the custom headers first
String headerValue = customHeaders.get(name);
if (headerValue != null){
return headerValue;
}
// else return from into the original wrapped object
return ((HttpServletRequest) getRequest()).getHeader(name);
}
@Override
public Enumeration getHeaderNames() {
// create a set of the custom header names
Set set = new HashSet<>(customHeaders.keySet());
// now add the headers from the wrapped request object
Enumeration e = ((HttpServletRequest) getRequest()).getHeaderNames();
while (e.hasMoreElements()) {
// add the names of the request headers into the list
String n = e.nextElement();
set.add(n);
}
// create an enumeration from the set and return
return Collections.enumeration(set);
}
}
5、定义验证失败后的处理类
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final String TOKEN_EXPRIED = "Filter_TokenExpried";
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ResultVO resultVO;
if (httpServletRequest.getAttribute(TOKEN_EXPRIED) != null) {
resultVO=ResultVO.fail(CoreConstants.TOKEN_EXPIRED);
}else {
resultVO=ResultVO.fail(CoreConstants.NEED_AUTHORITIES);
}
String resultJson = URLDecoder.decode(JSON.toJSONString(resultVO), "UTF-8");
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
httpServletResponse.setHeader("Content-Type", "application/json;charset=UTF-8");
httpServletResponse.setHeader("Access-Control-Allow-Headers", "x-requested-with,authorization-erp-fqkj,authorization-app-fqkj,authorization-manage-fqkj");
httpServletResponse.getWriter().write(resultJson);
}
}
二、在Spring Cloud网关中,通过ZuulFilter,把当前登录用户的基本信息注入到请求头中
@Component
public class ZuulAccessFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(ZuulAccessFilter.class);
private static final String HEADER_USER = "key_userinfo_in_http_header";
@Override
public String filterType() {
//前置过滤器
return "pre";
}
@Override
public int filterOrder() {
//优先级,数字越大,优先级越低
return 0;
}
@Override
public boolean shouldFilter() {
//是否执行该过滤器,true代表需要过滤
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String userInfoStr = request.getHeader(HEADER_USER);
if (!Strings.isNullOrEmpty(userInfoStr)) {
try {
ctx.addZuulRequestHeader(HEADER_USER, URLEncoder.encode(userInfoStr, "UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
return ctx;
}
}
三、在Spring Cloud微服务中,定义过滤器,解析网关传递的请求头,解析出当前访问用户的基本信息
public class TransmitUserInfoFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(TransmitUserInfoFeighClientIntercepter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.initUserInfo((HttpServletRequest) request);
chain.doFilter(request, response);
}
private void initUserInfo(HttpServletRequest request) {
String userJson = request.getHeader("key_userinfo_in_http_header");
if (StringUtils.isNotBlank(userJson)) {
try {
userJson = URLDecoder.decode(userJson, "UTF-8");
UserInfo userInfo = JSON.parseObject(userJson, UserInfo.class);
//将UserInfo放入上下文中
UserInfoContext.setUser(userInfo);
} catch (UnsupportedEncodingException e) {
log.error("init userInfo error", e);
}
}
}
@Override
public void destroy() {
}
}
四、定义拦截器,在服务间相互调用时,把访问用户的信息通过请求头的方式传递到被调用的微服务中
public class TransmitUserInfoFeighClientIntercepter implements RequestInterceptor {
private static final Logger log = LoggerFactory.getLogger(TransmitUserInfoFeighClientIntercepter.class);
@Override
public void apply(RequestTemplate requestTemplate) {
//从应用上下文中取出user信息,放入Feign的请求头中
UserInfo user = UserInfoContext.getUser();
if (user != null) {
try {
String userJson = JSON.toJSONString(user);
requestTemplate.header("KEY_USERINFO_IN_HTTP_HEADER", URLEncoder.encode(userJson, "UTF-8"));
} catch (UnsupportedEncodingException e) {
log.error("用户信息设置错误", e);
}
}
}
}