微服务架构,前后端分离目前已成为互联网项目开发的业界标准,其核心思想就是前端(APP、小程序、H5页面等)通过调用后端的API接口,提交及返回JSON数据进行交互。
在前后端分离项目中,首先要解决的就是登录及授权的问题。微服务架构下,传统的session认证限制了应用的扩展能力,无状态的JWT认证方法应运而生,该认证机制特别适用于分布式站点的单点登录(SSO)场景
需要了解SpringSecurity安全框架+jwt基本原理
<!--Security框架-->
org.springframework.boot</groupId>
spring-boot-starter-security</artifactId>
</dependency>
<!-- jwt -->
io.jsonwebtoken</groupId>
jjwt-api</artifactId>
0.10.6</version>
</dependency>
io.jsonwebtoken</groupId>
jjwt-impl</artifactId>
0.10.6</version>
</dependency>
io.jsonwebtoken</groupId>
jjwt-jackson</artifactId>
0.10.6</version>
</dependency>
#jwt
jwt:
header: authorization
# 令牌前缀
token-start-with: bearer
# 使用Base64对该令牌进行编码 (制定您的密钥) 256
base64-secret: ytkj62672000cnzzabcdefghijlsafsdgasysgfhfgsdhfg
# 令牌过期时间 此处单位/毫秒
token-validity-in-seconds: 14400000
package com.yuantiaokj.jwt.jwt;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* ************************************************************
* Copyright © 2020 远眺科技 Inc.All rights reserved. * **
* ************************************************************
*
* @program: redis-demo
* @description: jwt配置类
* @author: cnzz
* @create: 2020-05-26 08:55
**/
@Data
@Configuration
@ConfigurationProperties(prefix = "jwt")
public class JwtSecurityProperties {
/** Request Headers : Authorization */
private String header;
/** 令牌前缀,最后留个空格 Bearer */
private String tokenStartWith;
/** Base64对该令牌进行编码 */
private String base64Secret;
/** 令牌过期时间 此处单位/毫秒 */
private Long tokenValidityInSeconds;
/**返回令牌前缀 */
public String getTokenStartWith() {
return tokenStartWith + " ";
}
}
package com.yuantiaokj.jwt.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* ************************************************************
* Copyright © 2020 远眺科技 Inc.All rights reserved. * **
* ************************************************************
*
* @program: redis-demo
* @description: jwt Token工具类
* @author: cnzz
* @create: 2020-05-26 09:01
**/
@Slf4j
@Component
public class JwtTokenUtils implements InitializingBean {
private final JwtSecurityProperties jwtSecurityProperties;
private static final String AUTHORITIES_KEY = "auth";
private Key key;
public JwtTokenUtils(JwtSecurityProperties jwtSecurityProperties) {
this.jwtSecurityProperties = jwtSecurityProperties;
}
@Override
public void afterPropertiesSet() {
byte[] keyBytes = Decoders.BASE64.decode(jwtSecurityProperties.getBase64Secret());
this.key = Keys.hmacShaKeyFor(keyBytes);
}
public String createToken(String userId,String userName, List<String> permissionList) {
String newStr = permissionList.stream().collect(Collectors.joining(","));
return Jwts.builder()
.claim(AUTHORITIES_KEY, newStr)
.setId(userId)
.setSubject(userName)
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtSecurityProperties.getTokenValidityInSeconds()))
.compressWith(CompressionCodecs.DEFLATE)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
public Date getExpirationDateFromToken(String token) {
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody();
log.debug("我的filter|claims={}", claims);
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
log.debug("我的filter|authorities={}", authorities);
//HashMap map =(HashMap) claims.get("auth");
//User principal = new User(map.get("user").toString(),map.get("password").toString(), authorities);
String principal=(String)claims.get("sub");
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(key).parseClaimsJws(authToken);
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.info("Invalid JWT signature.");
e.printStackTrace();
} catch (ExpiredJwtException e) {
log.info("Expired JWT token.");
e.printStackTrace();
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token.");
e.printStackTrace();
} catch (IllegalArgumentException e) {
log.info("JWT token compact of handler are invalid.");
e.printStackTrace();
}
return false;
}
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
}
package com.yuantiaokj.jwt.jwt;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
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.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* ************************************************************
* Copyright © 2020 远眺科技 Inc.All rights reserved. * **
* ************************************************************
*
* @program: redis-demo
* @description: springSecurity配置类
* @author: cnzz
* @create: 2020-05-26 09:08
**/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtTokenUtils jwtTokenUtils;
public WebSecurityConfig(JwtAccessDeniedHandler jwtAccessDeniedHandler, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, JwtTokenUtils jwtTokenUtils) {
this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.jwtTokenUtils = jwtTokenUtils;
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 禁用 CSRF
.csrf().disable()
// 授权异常
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
// 防止iframe 造成跨域
.and()
.headers()
.frameOptions()
.disable()
// 不创建会话
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 放行静态资源
.antMatchers(
HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/webSocket/**"
).permitAll()
// 放行swagger
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/*/api-docs").permitAll()
// 放行文件访问
.antMatchers("/file/**").permitAll()
// 放行druid
.antMatchers("/druid/**").permitAll()
// 放行OPTIONS请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
//允许匿名及登录用户访问
.antMatchers("/LoginController/loginByUserNameAndPassword",
"/LoginController/loginByPhone",
"/LoginController/getQrCodeKey",
"/LoginController/getUserInfoByKey",
"/LoginController/qrCodelogin",
"/sysTimeController/getSysTimeMinute"
).permitAll()
// 所有请求都需要认证
.anyRequest().authenticated();
// 禁用缓存
httpSecurity.headers().cacheControl();
// 添加JWT filter
httpSecurity
.apply(new TokenConfigurer(jwtTokenUtils));
}
public class TokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final JwtTokenUtils jwtTokenUtils;
public TokenConfigurer(JwtTokenUtils jwtTokenUtils) {
this.jwtTokenUtils = jwtTokenUtils;
}
@Override
public void configure(HttpSecurity http) {
JwtAuthenticationTokenFilter customFilter = new JwtAuthenticationTokenFilter(jwtTokenUtils);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
}
package com.yuantiaokj.jwt.jwt;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* ************************************************************
* Copyright © 2020 远眺科技 Inc.All rights reserved. * **
* ************************************************************
*
* @program: redis-demo
* @description: token验证过滤器
* @author: cnzz
* @create: 2020-05-26 09:05
**/
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private JwtTokenUtils jwtTokenUtils;
public JwtAuthenticationTokenFilter(JwtTokenUtils jwtTokenUtils) {
this.jwtTokenUtils = jwtTokenUtils;
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
JwtSecurityProperties jwtSecurityProperties = SpringContextHolder.getBean(JwtSecurityProperties.class);
String requestRri = httpServletRequest.getRequestURI();
//获取request token
String token = null;
String bearerToken = httpServletRequest.getHeader(jwtSecurityProperties.getHeader());
log.debug("从requst获取的|bearerToken={}",bearerToken);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(jwtSecurityProperties.getTokenStartWith())) {
token = bearerToken.substring(jwtSecurityProperties.getTokenStartWith().length());
}
if (StringUtils.hasText(token) && jwtTokenUtils.validateToken(token)) {
Authentication authentication = jwtTokenUtils.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("set Authentication to security context for '{}', uri: {}", authentication.getName(), requestRri);
} else {
log.debug("no valid JWT token found, uri: {}", requestRri);
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
package com.yuantiaokj.jwt.jwt;
import com.alibaba.fastjson.JSON;
import com.yuantiaokj.commonmodule.base.SysRes;
import com.yuantiaokj.commonmodule.code.PubCode;
import com.yuantiaokj.commonmodule.exception.BizException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* ************************************************************
* Copyright © 2020 远眺科技 Inc.All rights reserved. * **
* ************************************************************
*
* @program: redis-demo
* @description: 认证失败
* @author: cnzz
* @create: 2020-05-26 09:00
**/
@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
log.debug("进入JwtAuthenticationEntryPoint|PC_999001_重新登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(SysRes.failure(new BizException(PubCode.PC_999001_重新登录))));
//throw new BizException(PubCode.PC_999001_重新登录);
//response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException==null?"Unauthorized":authException.getMessage());
}
}
package com.yuantiaokj.jwt.jwt;
import com.alibaba.fastjson.JSON;
import com.yuantiaokj.commonmodule.base.SysRes;
import com.yuantiaokj.commonmodule.code.PubCode;
import com.yuantiaokj.commonmodule.exception.BizException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* ************************************************************
* Copyright © 2020 远眺科技 Inc.All rights reserved. * **
* ************************************************************
*
* @program: redis-demo
* @description: 无权限访问类
* @author: cnzz
* @create: 2020-05-26 08:58
**/
@Component
@Slf4j
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
log.debug("JwtAccessDeniedHandler|PC_999401_无权限");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(SysRes.failure(new BizException(PubCode.PC_999401_无权限))));
}
}
package com.yuantiaokj.jwt.globalException;
import com.yuantiaokj.commonmodule.base.SysRes;
import com.yuantiaokj.commonmodule.code.PubCode;
import com.yuantiaokj.commonmodule.exception.BizException;
import com.yuantiaokj.commonmodule.exception.ValidationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
/**
* ************************************************************
* Copyright © 2020 远眺科技 Inc.All rights reserved. * **
* ************************************************************
*
* @program: redis-demo
* @description:
* @author: cnzz
* @create: 2020-05-28 18:34
**/
// 统一的异常类进行处理(把默认的异常返回信息改成自定义的异常返回信息)
// 当GlobalContrller抛出HospitalException异常时,将自动找到此类中对应的方法执行,并返回json数据给前台
@ControllerAdvice
@Slf4j
public class ExceptionController {
@ResponseBody
@ExceptionHandler(value = Exception.class) //异常处理器,处理HospitalException异常
public SysRes hanlerException(HttpServletRequest request, Exception e) {
e.printStackTrace();
return SysRes.exception(e);
}
//异常处理器,处理HospitalException异常
@ResponseBody
@ExceptionHandler(value = ValidationException.class)
public SysRes hanlerValidationException(HttpServletRequest request, ValidationException e) {
e.printStackTrace();
return SysRes.failure(e);
}
//异常处理器,处理HospitalException异常
@ResponseBody
@ExceptionHandler(value = BizException.class)
public SysRes hanlerBizException(HttpServletRequest request, BizException e) {
e.printStackTrace();
return SysRes.failure(e);
}
//AuthenticationException
@ResponseBody
@ExceptionHandler(value = AuthenticationException.class)
public SysRes hanlerAuthenticationException(HttpServletRequest request, AuthenticationException e) {
// e.printStackTrace();
log.debug("进入ExceptionController|PC_999001_重新登录");
return SysRes.failure(new BizException(PubCode.PC_999001_重新登录));
}
//AccessDeniedException
@ResponseBody
@ExceptionHandler(value = AccessDeniedException.class)
public SysRes hanlerAccessDeniedException(HttpServletRequest request, AccessDeniedException e) {
//e.printStackTrace();
log.debug("进入ExceptionController|PC_999401_无权限");
return SysRes.failure(new BizException(PubCode.PC_999401_无权限));
}
}
package com.yuantiaokj.jwt.controller;
import com.alibaba.fastjson.JSON;
import com.yuantiaokj.jwt.jwt.JwtTokenUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
/**
* ************************************************************
* Copyright © 2020 远眺科技 Inc.All rights reserved. * **
* ************************************************************
*
* @program: module-lib
* @description: 测试
* @author: cnzz
* @create: 2020-06-04 16:39
**/
@RestController
@Slf4j
@RequestMapping("/JwtTokenController")
public class JwtTokenController {
@Resource
JwtTokenUtils jwtTokenUtils;
@GetMapping("/getToken")
public String getToken() {
List<String> permissions =new ArrayList<>();
permissions.add("admin");
permissions.add("afds");
String token = jwtTokenUtils.createToken("1223", "abcc", permissions);
log.info("获取的|token={}",token);
return token;
}
@RequestMapping("/validPermission")
@PreAuthorize("hasAnyAuthority('admin')")
public String validPermission() {
log.info("进入|validPermission");
return "OK";
}
public static void main(String[] args) {
List<String> permissions =new ArrayList<>();
permissions.add("admin");
log.info(JSON.toJSONString(permissions));
}
}
后续会打包组件,开箱即用,期待,,,,