目录
前言
一、钻石段位springsecurity+springsession+redis缓存
1.yml配置将session存储到redis中,实现session共享
2.maven引入
3.原理分析
a.SessionRepositoryFilter拦截进行session存储介质的选择,可以是jdk缓存,或者数据库,也可以是redis缓存
b.SessionRepositoryRequestWrapper重写request
c.sessionRepository就是RedisOperationsSessionRepository
二、星耀段位springsecurity+oauth2+jwttoken
1.微服务架构图
2.代码实现(通过拦截实现用户信息服务间的传递)
a.网关拿到access_token查询是否存在认证信息
b.如果存在,则把用户信息转换成jwttoken放入请求头
c. A服务从头中获取jwttoken,解析出用户信息
d.A服务处理完业务逻辑,将jwttoken从新放入请求头中
3.疑问
总结
前言
青铜段位:单体应用,认证方式sessionId
白银段位:微服务应用,认证方式springsession+sessionId同步,存在延迟
钻石段位:微服务应用,认证方式springsession+redis分布式缓存,存在跨域问题以及CSRF攻击
星耀段位:微服务应用,认证方式springsecurity+oauth2+jwttoken+redis 方式
一、钻石段位springsecurity+springsession+redis缓存
1.yml配置将session存储到redis中,实现session共享
server:
port: 8080
servlet:
session:
timneout: 3000
spring:
application:
name: SpringSessionRedis
redis:
host: localhost
port: 6379
timeout: 3000
pool:
max-idle: 8
min-idle: 0
max-active: 8
max-wait: 1
session:
store-type: redis
redis:
#用于存储在redis中key的命名空间
flush-mode: on_save
#session更新策略,有ON_SAVE、IMMEDIATE,前者是在调用#SessionRepository#save(org.springframework.session.Session)时,在response commit前刷新缓存,
#后者是只要有任何更新就会刷新缓存
namespace: 'spring:session'
2.maven引入
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>1.0.1.RELEASE</version>
</dependency>
3.原理分析
a.SessionRepositoryFilter拦截进行session存储介质的选择,可以是jdk缓存,或者数据库,也可以是redis缓存
SessionRepositoryFilter -> doFilterInternal:
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
request, response, this.servletContext);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
wrappedRequest, response);
//包装 request,response
HttpServletRequest strategyRequest = this.httpSessionStrategy
.wrapRequest(wrappedRequest, wrappedResponse);
HttpServletResponse strategyResponse = this.httpSessionStrategy
.wrapResponse(wrappedRequest, wrappedResponse);
try {
filterChain.doFilter(strategyRequest, strategyResponse);
}
finally {
//更新session
wrappedRequest.commitSession();
}
}
b.SessionRepositoryRequestWrapper重写request
private S getSession(String sessionId) {
S session = SessionRepositoryFilter.this.sessionRepository
.getSession(sessionId);
if (session == null) {
return null;
}
session.setLastAccessedTime(System.currentTimeMillis());
return session;
}
// 重写 父类 getSession 方法
@Override
public HttpSessionWrapper getSession(boolean create) {
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
//从当前请求获取sessionId
String requestedSessionId = getRequestedSessionId();
if (requestedSessionId != null
&& getAttribute(INVALID_SESSION_ID_ATTR) == null) {
S session = getSession(requestedSessionId);
if (session != null) {
this.requestedSessionIdValid = true;
//对Spring session 进行包装(包装成HttpSession)
currentSession = new HttpSessionWrapper(session, getServletContext());
currentSession.setNew(false);
setCurrentSession(currentSession);
return currentSession;
}
else {
// This is an invalid session id. No need to ask again if
// request.getSession is invoked for the duration of this request
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
}
if (!create) {
return null;
}
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(System.currentTimeMillis());
//对Spring session 进行包装(包装成HttpSession)
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
c.sessionRepository就是RedisOperationsSessionRepository
二、星耀段位springsecurity+oauth2+jwttoken
1.微服务架构图
1.前端发起系统登录,网关接收到登录请求,转发到认证服务,认证服务通过生成一个access_token返回给前端,前端保存到前端缓存中,同时后端保存access_token与用户的信息到redis缓存
2. 当前端再次拿access_token发起请求时,网关首先从redis缓存中查询是否有认证信息,如果有,则拿到用户信息,生成jwttoken,放入请求头中,往后传
3.A服务从请求头中拿到jwttoken,解析出用户信息放入applicationContext中,供服务使用
4.A服务调用B服务,将jwttoken又放入请求头中,B服务拿到jwttoken解析用户信息,同样放入applicationcontext中,供服务使用
2.代码实现(通过拦截实现用户信息服务间的传递)
a.网关拿到access_token查询是否存在认证信息
@Component
public class GetUserDetailsFilter implements HelperFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(GetUserDetailsFilter.class);
private static final String BEARER_PREFIX = "Bearer ";
private GetUserDetailsService getUserDetailsService;
private ReadonlyRedisTokenStore tokenStore;
private GatewayHelperProperties helperProperties;
public GetUserDetailsFilter(GetUserDetailsService getUserDetailsService
, ReadonlyRedisTokenStore tokenStore,GatewayHelperProperties helperProperties) {
this.getUserDetailsService = getUserDetailsService;
this.tokenStore = tokenStore;
this.helperProperties=helperProperties;
}
@Override
public int filterOrder() {
return 40;
}
@Override
public boolean shouldFilter(RequestContext context) {
return context.getCustomUserDetails() == null;
}
@Override
@SuppressWarnings("unchecked")
public boolean run(RequestContext context) {
//校验客户端是否带有token
String accessToken = context.request.accessToken;
if (ObjectUtils.isEmpty(accessToken)) {
context.response.setStatus(CheckState.PERMISSION_ACCESS_TOKEN_NULL);
context.response.setMessage("Access_token is empty, Please login and set access_token by HTTP header 'Authorization'");
return Boolean.FALSE;
}
//判断是否是被踢下线用户
if (org.springframework.util.StringUtils.startsWithIgnoreCase(accessToken, BEARER_PREFIX)) {
accessToken = accessToken.substring(BEARER_PREFIX.length());
}
boolean exists = tokenStore.existsOfflineAccessToken(accessToken);
if (exists) {
LOGGER.warn("已在其他地方登录,记录了下线token");
//下线token存在,这是被踢下去的用户
context.response.setStatus(CheckState.PERMISSION_ACCESS_TOKEN_OFFLINE);
context.response.setMessage("Access_token is offline, Please login again");
return Boolean.FALSE;
}
//验证用户是否token已经过期
CustomUserDetailsWithResult result = getUserDetailsService.getUserDetails(accessToken);
if (result.getCustomUserDetails() == null) {
context.response.setStatus(result.getState());
context.response.setMessage(result.getMessage());
}
}
b.如果存在,则把用户信息转换成jwttoken放入请求头
@Component
public class AddJwtFilter implements HelperFilter {
private final ObjectMapper objectMapper = new ObjectMapper();
private Signer jwtSigner;
public AddJwtFilter(Signer jwtSigner) {
this.jwtSigner = jwtSigner;
}
@Override
public int filterOrder() {
return 50;
}
@Override
public boolean shouldFilter(RequestContext context) {
return true;
}
@Override
public boolean run(RequestContext context) {
try {
String token = objectMapper.writeValueAsString(context.getCustomUserDetails());
String jwt = "Bearer " + JwtHelper.encode(token, jwtSigner).getEncoded();
context.response.setJwt(jwt);
return true;
} catch (JsonProcessingException e) {
context.response.setStatus(CheckState.EXCEPTION_GATEWAY_HELPER);
context.response.setMessage("gateway helper error happened: " + e.toString());
return false;
}
}
}
c. A服务从头中获取jwttoken,解析出用户信息
public class JwtTokenFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
try {
//解析认证信息
Authentication authentication = this.tokenExtractor.extract(httpRequest);
if (authentication == null) {
if (this.isAuthenticated()) {
LOGGER.debug("Clearing security context.");
SecurityContextHolder.clearContext();
}
LOGGER.debug("No Jwt token in request, will continue chain.");
((HttpServletResponse) response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "No Jwt token in request.");
return;
} else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
needsDetails.setDetails(new OAuth2AuthenticationDetails(httpRequest));
}
//再次认证
Authentication authResult = this.authenticate(authentication);
LOGGER.debug("Authentication success: {}", authResult);
//放入上下文中
SecurityContextHolder.getContext().setAuthentication(authResult);
}
chain.doFilter(request, response);
} catch (OAuth2Exception e) {
LOGGER.debug("Authentication request failed: ", e);
((HttpServletResponse) response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT token.");
} finally {
SecurityContextHolder.clearContext();
}
}
protected Authentication authenticate(Authentication authentication) {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
} else {
String token = (String) authentication.getPrincipal();
OAuth2Authentication auth = this.tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
} else {
if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
if (!details.equals(auth.getDetails())) {
details.setDecodedDetails(auth.getDetails());
}
}
auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
return auth;
}
}
}
}
d.A服务处理完业务逻辑,将jwttoken从新放入请求头中
public class JwtRequestInterceptor implements FeignRequestInterceptor {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final String OAUTH_TOKEN_PREFIX = "Bearer ";
private Signer signer;
private CoreProperties coreProperties;
public JwtRequestInterceptor(CoreProperties coreProperties) {
this.coreProperties = coreProperties;
}
@PostConstruct
private void init() {
signer = new MacSigner(coreProperties.getOauthJwtKey());
}
@Override
public int getOrder() {
return -1000;
}
@Override
public void apply(RequestTemplate template) {
String token = null;
try {
if (SecurityContextHolder.getContext() != null && SecurityContextHolder.getContext().getAuthentication() != null) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
if (StringUtils.isNoneBlank(details.getTokenType(), details.getTokenValue())) {
token = details.getTokenType() + " " + details.getTokenValue();
} else if (details.getDecodedDetails() instanceof CustomUserDetails) {
token = OAUTH_TOKEN_PREFIX + JwtHelper.encode(OBJECT_MAPPER.writeValueAsString(details.getDecodedDetails()), signer).getEncoded();
}
} else if (authentication.getPrincipal() instanceof CustomUserDetails) {
token = OAUTH_TOKEN_PREFIX + JwtHelper.encode(OBJECT_MAPPER.writeValueAsString(authentication.getPrincipal()), signer).getEncoded();
}
}
if (token == null) {
LOGGER.debug("Feign request set Header Jwt_Token, no member token found, use AnonymousUser default.");
token = OAUTH_TOKEN_PREFIX + JwtHelper.encode(OBJECT_MAPPER.writeValueAsString(DetailsHelper.getAnonymousDetails()), signer).getEncoded();
}
} catch (Exception e) {
LOGGER.error("generate jwt token failed {}", e.getMessage());
}
template.header(RequestVariableHolder.HEADER_JWT, token);
setLabel(template);
}
private void setLabel(RequestTemplate template) {
String label = RequestVariableHolder.LABEL.get();
if (label != null && label.trim().length() > 0) {
template.header(RequestVariableHolder.HEADER_LABEL, label);
}
}
}
3.疑问
1.jwttoken,如果只用jwttoken保存用户信息,客户端保存即可,服务无需保存,那么退出注销怎么做?
2.上述架构,前端与服务集群后端使用access_token认证,后端保存了access_token与用户信息在redis缓存,服务间用jwttoken传递认证信息,优势在哪里?
总结
这种基于token的认证方式相比传统的session认证方式更节约服务器资源,并且对移动端和分布式更加友好。其优点如下:
支持跨域访问:cookie是无法跨域的,而token由于没有用到cookie(前提是将token放到请求头中),所以跨域后不会存在信息丢失问题
无状态:token机制在服务端不需要存储session信息,因为token自身包含了所有登录用户的信息,所以可以减轻服务端压力
更适用于移动端:当客户端是非浏览器平台时,cookie是不支持的,采用token认证方式会简单很多
无需考虑CSRF:由于不再依赖cookie,所以采用token认证方式不会发生CSRF,所以也就无需考虑CSRF的防御