目录
GlobalExceptionHandler 全局异常处理器
jwt+时间戳+责任链模式实现对外接口
需求:系统需要提供对外接口,客户可以通过接口获取我们系统的数据
方案:使用shiro+jwt,用户通过用户名和密码访问我们的登录接口/login获取token,访问其他api时携带token和timestamp(防止重放攻击)(注:对接口安全性要求高的可以另选其他方案)
TokenService——jwt工具类
@Component
public class TokenService {
private static final Logger log = LoggerFactory.getLogger(TokenService.class);
// 令牌自定义标识
@Value("${token.header}")
private String header;
// 令牌秘钥
@Value("${token.secret}")
private String secret;
// 令牌有效期(默认30)
@Value("${token.expireTime}")
private int expireTime;
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
private static final long MILLIS_HOUR = 60 * MILLIS_MINUTE;
/**
* 创建token
*
* @param claims token的payload
* @return token
*/
public String createToken(Map<String, Object> claims) {
// Map<String, Object> claims = new HashMap<>();
// claims.put(Constants.TOKEN_CLAIM_USERNAME, userLoginInfo.getUserName());
// claims.put(Constants.TOKEN_CLAIM_PASSWORD, userLoginInfo.getPassWord());
return Jwts.builder()
.setClaims(claims)
//设置token过期时间,默认为2h
.setExpiration(new Date(System.currentTimeMillis() + expireTime * MILLIS_HOUR))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 解析token
*
* @param token
* @return 存储用户账号密码的claim
*/
public Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
Claims claims = new DefaultClaims().setExpiration(new Date(System.currentTimeMillis()));
return claims;
} catch (Exception e) {
return null;
}
}
/**
* 校验token
*
* @param token
* @return true 有效, false 无效
*/
public boolean validateToken(String token) {
return parseToken(token) != null;
}
/**
* 判断token是否过期
*
* @param token
* @return
*/
public boolean expired(String token) {
return parseToken(token).getExpiration().before(new Date());
}
}
责任链模式
创建责任链的认证接口
接口继承Order,然后责任链的结点实现以下接口后,spring容器注入bean时通过Order对责任链结点进行排序,这样很好的生成了责任链
public interface SecurityVerificationHandler extends Ordered {
/**
* 请求校验
*/
void handler(HttpServletRequest request) throws Exception;
}
实现参数校验逻辑
/**
* 责任链结点--参数校验
*/
@Component
public class ParamVerificationHandler implements SecurityVerificationHandler {
@Override
public void handler(HttpServletRequest request) throws ParamException {
// 时间戳
// if(StringUtils.isEmpty((String) request.getHeader(Constants.TIMESTAMP))){
// throw new ParamException("参数" + Constants.TIMESTAMP + ": " + ErrorConstants.TIMESTAMP_NULL);
// }
// token
if (StringUtils.isEmpty(request.getHeader(Constants.AUTHORIZATION))) {
throw new ParamException("参数" + Constants.AUTHORIZATION + ": " + ErrorConstants.TOKEN_NULL);
}
}
/**
* 责任链结点的优先级,间隔尽量大,方便以后扩展
* @return
*/
@Override
public int getOrder() {
return 10;
}
}
/**
* 责任链结点--时间戳校验
*/
//@Component
public class TimestampVerificationHandler implements SecurityVerificationHandler {
@Override
public void handler(HttpServletRequest request) throws TimestampException {
String timestamp = request.getHeader(Constants.TIMESTAMP);
if(!validateTimestamp(timestamp)){
throw new TimestampException("参数" + Constants.TIMESTAMP + ": " + ErrorConstants.TIMESTAMP_FORMAT_ERROR);
}
long diff = System.currentTimeMillis() - Long.parseLong(timestamp);
if(diff < 0 || diff > Constants.REPLAY_ATTACK_INTERVAL){
throw new TimestampException( ErrorConstants.REPLAY_ATTACK);
}
}
/**
* 判断时间戳格式
* @param timestamp
* @return
*/
public boolean validateTimestamp(String timestamp) {
long nowTime = System.currentTimeMillis();
boolean flag = false;
try {
long l = Long.parseLong(timestamp);
} catch (NumberFormatException e) {
return false;
}
if (String.valueOf(nowTime).length() != timestamp.length()) {
return false;
}
return true;
}
/**
* 责任链结点的优先级,间隔尽量大,方便以后扩展
* @return
*/
@Override
public int getOrder() {
return 20;
}
}
/**
* 责任链结点--token校验
*/
@Component
public class TokenVerificationHandler implements SecurityVerificationHandler {
@Autowired
private TokenService tokenService;
@Override
public void handler(HttpServletRequest request) throws TokenException {
String token = request.getHeader(Constants.AUTHORIZATION);
if(!tokenService.validateToken(token)){
throw new TokenException(ErrorConstants.TOKEN_UNVALIDEATED);
}
if(tokenService.expired(token)){
throw new TokenException(ErrorConstants.TOKEN_EXPIRED);
}
Claims claims = tokenService.parseToken(token);
String username = (String) claims.get(Constants.USERNAME);
String password = (String) claims.get(Constants.PASSWORD);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken userToken = new UsernamePasswordToken(username, password);
try {
subject.login(userToken);
} catch (Exception e) {
throw new TokenException(ErrorConstants.TOKEN_UNVALIDEATED);
}
}
/**
* 责任链结点的优先级,间隔尽量大,方便以后扩展
* @return
*/
@Override
public int getOrder() {
return 30;
}
}
责任链上下文
/**
* 责任链上下文
*/
@Component
public class SecurityVerificationChain {
private final List<SecurityVerificationHandler> securityVerificationHandlers;
@Autowired
public SecurityVerificationChain(List<SecurityVerificationHandler> securityVerificationHandlers) {
this.securityVerificationHandlers = securityVerificationHandlers;
}
public void handler(HttpServletRequest request) throws Exception {
// 责任链结点已按书序排列好,直接遍历,顺序校验
for (SecurityVerificationHandler handler : securityVerificationHandlers) {
handler.handler(request);
}
}
}
JwtAuthorizationFilter
用于shiro框架的自定义的jwt过滤器,大家可以自定义realm的逻辑,这里不展开了
public class JwtAuthorizationFilter extends AccessControlFilter {
private SecurityVerificationChain securityVerificationChain = SpringUtils.getBean(SecurityVerificationChain.class);
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
// 异常
Exception ex = null;
try {
securityVerificationChain.handler(WebUtils.toHttp(request));
} catch (Exception e) {
ex = e;
}
// 抛一个异常, 全局异常处理
if (StringUtils.isNotNull(ex)) {
HandlerExceptionResolver handlerExceptionResolver = (HandlerExceptionResolver) SpringUtils.getBean("handlerExceptionResolver");
handlerExceptionResolver.resolveException(
WebUtils.toHttp(request),
WebUtils.toHttp(response),
null,
ex
);
return false;
}
return true;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
return false;
}
}
ShiroConfig
记得将jwt过滤器配置到shiro里面哈
/**
* Shiro配置类
*/
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterBean = new ShiroFilterFactoryBean();
shiroFilterBean.setSecurityManager(securityManager);
// 配置自定义过滤器
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("jwtFilter", new JwtAuthorizationFilter());
shiroFilterBean.setFilters(filters);
//配置路径过滤器 anon 无需登陆验证,直接访问; acthc 访问需要进行登录验证z`
Map<String, String> filterMap = new LinkedHashMap<>();
// ......
filterMap.put("/**", "jwtFilter");
shiroFilterBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterBean;
}
}
GlobalExceptionHandler 全局异常处理器
@RestControllerAdvice
public class GlobalExceptionHandler {
//日志打印类
private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionCatch.class);
// ParamException异常类
@ExceptionHandler(ParamException.class)
public ResponseResult customException(ParamException e){
//打印日志
LOGGER.error("catch exception : {}\r\nexception: ",e.getMessage(), e);
return ResponseResult.error(e.getMessage());
}
// ParamException异常类
@ExceptionHandler(TimestampException.class)
public ResponseResult customException(TimestampException e){
//打印日志
LOGGER.error("catch exception : {}\r\nexception: ",e.getMessage(), e);
return ResponseResult.error(e.getMessage());
}
// token异常类
@ExceptionHandler(TokenException.class)
public ResponseResult customException(TokenException e){
//打印日志
LOGGER.error("catch exception : {}\r\nexception: ",e.getMessage(), e);
return ResponseResult.error(401, e.getMessage());
}
// 其他异常
@ExceptionHandler(Exception.class)
public HttpResult customException(Exception e){
//打印日志
LOGGER.error("catch exception : {}\r\nexception: ",e.getMessage(), e);
//创建响应类,也就是最终传给页面的数据
HttpResult response = HttpResult.error(e.getMessage().toString());
return response;
}
}
参考连接
结语:这个方案实现了简单的token+timestamp+责任链设计的对外接口设计,比较简陋,大家可以尝试更好的解决方案