
一.首先用户登录成功之后关联jwt,并返回jwt
1.实现AuthenticationSuccessHandler
2.在AuthenticationSuccessHandler的handle方法中生成令牌并且把user信息写入jwt中,写入缓存以供前端传过来时通过令牌获取用户(这里不使用前端传过来的jwt里的用户信息,仅仅是用令牌来比对是否已存在该令牌并通过缓存获取用户和权限),这里最好给个有效时间,然后把jwt放入header中返回给前端,以后前端就用它来调用接口
代码:
public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
//private final RequestCache requestCache = new HttpSessionRequestCache(); private final ObjectMapper jacksonObjectMapper; private final RedisTemplateredisTemplate; public JwtAuthenticationSuccessHandler(ObjectMapper jacksonObjectMapper, RedisTemplate redisTemplate) { this.jacksonObjectMapper = jacksonObjectMapper; this.redisTemplate = redisTemplate; } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { log.info("JwtAuthenticationSuccessHandler=success"); clearAuthenticationAttributes(request); handle(response,authentication); } protected final void clearAuthenticationAttributes(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) return; session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); } protected void handle(HttpServletResponse response, Authentication authentication) throws IOException { if (response.isCommitted()) { log.debug("Response has already been committed."); return; } User sysUser=(User)authentication.getPrincipal(); sysUser.setClazz(authentication.getClass()); //AuthenticationAdapter authenticationAdapter=AuthenticationAdapter.authentication2AuthenticationAdapter(authentication); String authOfjson=jacksonObjectMapper.writevalueAsString(sysUser); String subject= UUID.randomUUID().toString(); String authOfjwt= JWTHS256.buildJWT(subject,authOfjson); response.addHeader("jwt",authOfjwt); //跨域时允许header携带jwt response.addHeader("Access-Control-Expose-Headers" ,"jwt"); redisTemplate.boundValueOps(SecurityConstants.getJwtKey(subject)).set("w",60, TimeUnit.MINUTES); response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); User returnSysUser=new User(); returnSysUser .setName(sysUser.getName()) .setCurrentOrg(sysUser.getCurrentOrg()) .setOrgIdMapRoleList(sysUser.getOrgIdMapRoleList()); out.write(jacksonObjectMapper.writevalueAsString(new ReturnObject<>(this.getClass().getName(),ConstantOfReturnCode.GLOBAL_RESULT_SUCESS,"登录成功",returnSysUser))); out.flush(); out.close(); }
}
3.在SpringSecurity的配置中注入:
http
.formLogin()
.successHandler(new JwtAuthenticationSuccessHandler(jacksonObjectMapper, redisTemplate))
这样调用成功了就会走这边处理
二.接下来就是前端传到后台时怎么处理,有两种方式,基本原理都是:在request.getHeader中获取jwt的令牌,然后再通过令牌从缓存中获取jwt),然后从缓存里得来的jwt里获取用户和权限,以此通过判断用户权限来控制用户对接口的使用
1.过滤器,在SpringSecurity的任意一个过滤器都可以
2.使用SpringSecurity的http.setSharedObject(SecurityContextRepository.class, new JwtSecurityContextRepository(redisTemplate, jacksonObjectMapper));
就是把SecurityContextRepository设成可共享的对象
代码:
package com.lc.yangzi.security.component.jwt;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lc.yangzi.security.component.SecurityConstants;
import com.lc.yangzi.security.domain.User;
import com.lc.yangzi.security.utility.JWTHS256;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
//import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.SecurityContextRepository;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Slf4j
public class JwtSecurityContextRepository implements SecurityContextRepository {
protected final Log logger = LogFactory.getLog(this.getClass());
private final RedisTemplateredisTemplate; private final ObjectMapper jacksonObjectMapper; public JwtSecurityContextRepository(RedisTemplate redisTemplate, ObjectMapper jacksonObjectMapper) { this.redisTemplate = redisTemplate; this.jacksonObjectMapper = jacksonObjectMapper; } @Override public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { HttpServletRequest request = requestResponseHolder.getRequest(); return readSecurityContextFromJWT(request); } @Override public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { } @Override public boolean containsContext(HttpServletRequest request) { return false; } private SecurityContext readSecurityContextFromJWT(HttpServletRequest request) { SecurityContext context=generateNewContext(); String authenticationOfjwt=request.getHeader("jwt"); if(StringUtils.isNotBlank(authenticationOfjwt)){ try{ Map map= JWTHS256.vaildToken(authenticationOfjwt); if(Objects.nonNull(map)&&map.size()==2){ String subject=(String)map.get("subject"); Boolean isExp=redisTemplate.hasKey(SecurityConstants.getJwtKey(subject)); if(Objects.nonNull(isExp)&&isExp){//redis key 未过期 redisTemplate.expire(SecurityConstants.getJwtKey(subject),60, TimeUnit.MINUTES);//延期 String obj=(String)map.get("claim"); //AuthenticationAdapter authenticationAdapter=jacksonObjectMapper.readValue(obj, new TypeReference<>(){}); //Authentication authentication=AuthenticationAdapter.authenticationAdapter2Authentication(authenticationAdapter); //Authentication authentication=jacksonObjectMapper.readValue(obj, new TypeReference<>(){}); //Authentication authentication=jacksonObjectMapper.readValue(obj,Authentication.class); User sysUser=jacksonObjectMapper.readValue(obj, new TypeReference (){}); Authentication authentication=new UsernamePasswordAuthenticationToken(sysUser,null,sysUser.getAuthorities()); context.setAuthentication(authentication); //if(obj instanceof Authentication){ //context.setAuthentication((Authentication)obj); //}else log.error("jwt包含authentication的数据非法"); }else log.error("jwt数据过期"); }else log.error("jwt数据非法"); }catch (Exception e){ e.printStackTrace(); logger.error(e.getLocalizedMessage()); } }else{ if (logger.isDebugEnabled()) { logger.debug("No JWT was available from the HttpServletRequestHeader!"); } } return context; } protected SecurityContext generateNewContext() { return SecurityContextHolder.createEmptyContext(); }
}
三.这里有个小知识点:
1.以上SpringSecurity的配置和初始化里有个适配器和关键,相当于一个过滤器,
2.这里有个对适配器的解释:
Adapter是连接后端数据和前端显示的适配器接口,是数据和UI(View)之间一个重要的纽带。在常见的View(List View,Grid View)等地方都需要用到Adapter。
3.也就是说每次前端请求后端都要通过适配器,每次都会调用一边
代码
@Configuration
@Order(2)
public class JWTSecurityConfig extends WebSecurityConfigurerAdapter
@Override
protected void configure(HttpSecurity http) throws Exception {
//源码:SecurityContextConfigurer
//源码:SessionManagementConfigurer
http
.requestMatchers().antMatchers("/**")
.and()
.anonymous().disable()
.authorizeRequests()
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
.anyRequest()
.authenticated()
.withObjectPostProcessor(new ObjectPostProcessor() {
public O postProcess(O fsi) {
//fsi.setRejectPublicInvocations(true); // 拒绝公共URL调用.源码:AbstractSecurityInterceptor,当请求的URL没有配置或者没有对应到角色时,不给予放行抛出异常!
fsi.setAccessDecisionManager(customAccessDecisionManager());
//使用默认的DefaultFilterInvocationSecuritymetadataSource及数据库配置的CustomSecuritymetadataSource
fsi.setSecuritymetadataSource(customSecuritymetadataSource(securityService, fsi.getSecuritymetadataSource()));
return fsi;
}
});
http
.addFilterBefore(
new KaptchaAuthenticationFilter(customAuthenticationFailureHandler(jacksonObjectMapper), securityConfigurator, redisTemplate),
UsernamePasswordAuthenticationFilter.class);
http
.formLogin()
.loginProcessingUrl(securityConfigurator.getLoginProcessingUrl())
.usernameParameter(securityConfigurator.getUsernameParameter())
.passwordParameter(securityConfigurator.getPasswordParameter())
.permitAll()
.failureHandler(customAuthenticationFailureHandler(jacksonObjectMapper))
.successHandler(new JwtAuthenticationSuccessHandler(jacksonObjectMapper, redisTemplate))
.withObjectPostProcessor(new ObjectPostProcessor() {
public O postProcess(O upaf) {
//设置登录请求匹配,默认的路径为配置的路基,默认的方法为Post,详见:AbstractAuthenticationProcessingFilter及其子类:UsernamePasswordAuthenticationFilter
upaf.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(securityConfigurator.getLoginProcessingUrl()));
//UsernamePasswordAuthenticationFilter类的attemptAuthentication方法
upaf.setPostonly(false);
return upaf;
}
});
http
.exceptionHandling()
.accessDeniedHandler(customAccessDeniedHandler(jacksonObjectMapper))
.authenticationEntryPoint(customAuthenticationEntryPoint(jacksonObjectMapper));
http
.logout()
.logoutUrl(securityConfigurator.getLogoutUrl())
.logoutSuccessHandler((request, response, authentication) -> {
ReturnObject