一.首先用户登录成功之后关联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 RedisTemplate redisTemplate;
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 RedisTemplate redisTemplate;
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