前言

需要把Web应用做成无状态的,即服务器端无状态,就是说服务器端不会存储像会话这种东西,而是每次请求时access_token进行资源访问。这里我们将使用 JWT 1,基于散列的消息认证码,使用一个密钥和一个消息作为输入,生成它们的消息摘要。该密钥只有服务端知道。访问时使用该消息摘要进行传播,服务端然后对该消息摘要进行验证。

认证步骤

  1. 客户端第一次使用用户名密码访问认证服务器,服务器验证用户名和密码,认证成功,使用用户密钥生成JWT并返回
  2. 之后每次请求客户端带上JWT
  3. 服务器对JWT进行验证

自定义 jwt 拦截器

/**
* oauth2拦截器,现在改为 JWT 认证
*/
public class OAuth2Filter extends FormAuthenticationFilter {
/**
* 设置 request 的键,用来保存 认证的 userID,
*/
private final static String USER_ID = "USER_ID";
@Resource
private JwtUtils jwtUtils; /**
* logger
*/
private static final Logger LOGGER = LoggerFactory.getLogger(OAuth2Filter.class); /**
* shiro权限拦截核心方法 返回true允许访问resource,
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
String token = getRequestToken((HttpServletRequest) request);
try {
// 检查 token 有效性
//ExpiredJwtException JWT已过期
//SignatureException JWT可能被篡改
Jwts.parser().setSigningKey(jwtUtils.getSecret()).parseClaimsJws(token).getBody();
} catch (Exception e) {
// 身份验证失败,返回 false 将进入onAccessDenied 判断是否登陆。
onLoginFail(response);
return false;
}
Long userId = getUserIdFromToken(token);
// 存入到 request 中,在后面的业务处理中可以使用
request.setAttribute(USER_ID, userId);
return true;
} /**
* 当访问拒绝时是否已经处理了;
* 如果返回true表示需要继续处理;
* 如果返回false表示该拦截器实例已经处理完成了,将直接返回即可。
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
return executeLogin(request, response);
} else {
return true;
}
} else {
onLoginFail(response);
return false;
}
} /**
* 鉴定失败,返回错误信息
* @param token
* @param e
* @param request
* @param response
* @return
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
try {
((HttpServletResponse) response).setStatus(HttpStatus.BAD_REQUEST.value());
response.getWriter().print("账号活密码错误");
} catch (IOException e1) {
LOGGER.error(e1.getMessage(), e1);
}
return false;
} /**
* token 认证失败
*
* @param response
*/
private void onLoginFail(ServletResponse response) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
((HttpServletResponse) response).setStatus(HttpStatus.UNAUTHORIZED.value());
try {
response.getWriter().print("没有权限,请联系管理员授权");
} catch (IOException e) {
LOGGER.error(e.getMessage(), e);
}
} /**
* 获取请求的token
*/
private String getRequestToken(HttpServletRequest httpRequest) {
//从header中获取token
String token = httpRequest.getHeader(jwtUtils.getHeader());
//如果header中不存在token,则从参数中获取token
if (StringUtils.isBlank(token)) {
return httpRequest.getParameter(jwtUtils.getHeader());
}
if (StringUtils.isBlank(token)) {
// 从 cookie 获取 token
Cookie[] cookies = httpRequest.getCookies();
if (null == cookies || cookies.length == 0) {
return null;
}
for (Cookie cookie : cookies) {
if (cookie.getName().equals(jwtUtils.getHeader())) {
token = cookie.getValue();
break;
}
}
}
return token;
} /**
* 根据 token 获取 userID
*
* @param token token
* @return userId
*/
private Long getUserIdFromToken(String token) {
if (StringUtils.isBlank(token)) {
throw new KCException("无效 token", HttpStatus.UNAUTHORIZED.value());
}
Claims claims = jwtUtils.getClaimByToken(token);
if (claims == null || jwtUtils.isTokenExpired(claims.getExpiration())) {
throw new KCException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
}
return Long.parseLong(claims.getSubject());
} }

将自定义shiro拦截器,设置到 ShiroFilterFactoryBean 中,然后将需要进行权限验证的 path 进行设置拦截过滤。

登陆

    @PostMapping("/login")
@ApiOperation("系统登陆")
public ResponseEntity<String> login(@RequestBody SysUserLoginForm userForm) {
String kaptcha = ShiroUtils.getKaptcha(Constants.KAPTCHA_SESSION_KEY);
if (!userForm.getCaptcha().equalsIgnoreCase(kaptcha)) {
throw new KCException("验证码不正确!");
}
UsernamePasswordToken token = new UsernamePasswordToken(userForm.getUsername(), userForm.getPassword());
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token); //账号锁定
if (getUser().getStatus() == SysConstant.SysUserStatus.LOCK) {
throw new KCException("账号已被锁定,请联系管理员");
}
// 登陆成功后直接返回 token ,然后后续放到 header 中认证
return ResponseEntity.status(HttpStatus.OK).body(jwtUtils.generateToken(getUserId()));
}

JwtUtils

我前面给 jwt 设置了三个参数

# jwt 配置
jwt:
# 加密密钥
secret: 61D73234C4F93E03074D74D74D1E39D9 #blog.wuwii.com
# token有效时长
expire: 7 # 7天,单位天
# token 存在 header 中的参数
header: token

jwt 工具类的编写

@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtUtils {
/**
* logger
*/
private Logger logger = LoggerFactory.getLogger(JwtUtils.class); /**
* 密钥
*/
private String secret;
/**
* 有效期限
*/
private int expire;
/**
* 存储 token
*/
private String header; /**
* 生成jwt token
*
* @param userId 用户ID
* @return token
*/
public String generateToken(long userId) {
Date nowDate = new Date(); return Jwts.builder()
.setHeaderParam("typ", "JWT")
// 后续获取 subject 是 userid
.setSubject(userId + "")
.setIssuedAt(nowDate)
.setExpiration(DateUtils.addDays(nowDate, expire))
// 这里我采用的是 HS512 算法
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
} /**
* 解析 token,
* 利用 jjwt 提供的parser传入秘钥,
*
* @param token token
* @return 数据声明 Map<String, Object>
*/
public Claims getClaimByToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
return null;
}
} /**
* token是否过期
*
* @return true:过期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
} public String getSecret() {
return secret;
} public void setSecret(String secret) {
this.secret = secret;
} public int getExpire() {
return expire;
} public void setExpire(int expire) {
this.expire = expire;
} public String getHeader() {
return header;
} public void setHeader(String header) {
this.header = header;
}
}

总结

由于 JWT 这种方式,服务端不需要保存任何状态,所以服务端不需要使用 session 保存用户信息,单元测试也比较方便,虽然中间转码解码会消耗一些性能,但是影响不大,还比较方便的应用在 SSO 2。


  1. JSON WEB Token ↩
  2. Single Sign On ↩

学习Spring Boot:(十六)使用Shiro与JWT 实现认证服务的更多相关文章

  1. Spring Boot(十六):使用Jenkins部署Spring Boot

    Spring Boot(十六):使用Jenkins部署Spring Boot jenkins是devops神器,介绍如何安装和使用jenkins部署Spring Boot项目 jenkins搭建 部署 ...

  2. (转)Spring Boot(十六):使用 Jenkins 部署 Spring Boot

    http://www.ityouknow.com/springboot/2017/11/11/spring-boot-jenkins.html enkins 是 Devops 神器,本篇文章介绍如何安 ...

  3. Spring Boot(十六):使用 Jenkins 部署 Spring Boot

    Jenkins 是 Devops 神器,本篇文章介绍如何安装和使用 Jenkins 部署 Spring Boot 项目 Jenkins 搭建.部署分为四个步骤: 第一步,Jenkins 安装 第二步, ...

  4. spring boot(十六)使用Jenkins部署spring boot

    jenkins是devops神器,本篇文章介绍如何安装和使用jenkins部署Spring Boot项目 jenkins搭建 部署分为三个步骤: 第一步,jenkins安装 第二步,插件安装和配置 第 ...

  5. Spring Boot 最简单整合Shiro+JWT方式

    简介 目前RESTful大多都采用JWT来做授权校验,在Spring Boot 中可以采用Shiro和JWT来做简单的权限以及认证验证,在和Spring Boot集成的过程中碰到了不少坑.便结合自身以 ...

  6. 学习Spring Boot:(二十六)使用 RabbitMQ 消息队列

    前言 前面学习了 RabbitMQ 基础,现在主要记录下学习 Spring Boot 整合 RabbitMQ ,调用它的 API ,以及中间使用的相关功能的记录. 相关的可以去我的博客/RabbitM ...

  7. 学习Spring Boot:(二十五)使用 Redis 实现数据缓存

    前言 由于 Ehcache 存在于单个 java 程序的进程中,无法满足多个程序分布式的情况,需要将多个服务器的缓存集中起来进行管理,需要一个缓存的寄存器,这里使用的是 Redis. 正文 当应用程序 ...

  8. Spring Boot(十四):spring boot整合shiro-登录认证和权限管理

    Spring Boot(十四):spring boot整合shiro-登录认证和权限管理 使用Spring Boot集成Apache Shiro.安全应该是互联网公司的一道生命线,几乎任何的公司都会涉 ...

  9. 学习 Spring Boot 知识看这一篇就够了

    从2016年因为工作原因开始研究 Spring Boot ,先后写了很多关于 Spring Boot 的文章,发表在技术社区.我的博客和我的公号内.粗略的统计了一下总共的文章加起来大概有六十多篇了,其 ...

  10. Spring Boot入门(六):使用MyBatis访问MySql数据库(注解方式)

    本系列博客记录自己学习Spring Boot的历程,如帮助到你,不胜荣幸,如有错误,欢迎指正! 本篇博客我们讲解下在Spring Boot中使用MyBatis访问MySql数据库的简单用法. 1.前期 ...

随机推荐

  1. Mapnik初学笔记

    前言:夏天总是感觉想要睡觉,一心想颓废的我却要一周六天都要处于工作状态,但有些事虽然麻烦,但还是要去做,不由得想起火影忍者里面鹿丸这一个角色,有时候真能理解他的心理状态,或许我应该向他学习:善于思考的 ...

  2. Linux下离线安装Docker

    Linux下离线安装Docker 一.基础环境 1.操作系统:CentOS 7.3 2.Docker版本:18.06.1 官方下载地址(打不开可能需要***) 3.百度云Docker 18.06.1地 ...

  3. 20155207 EXP6 信息搜集与漏洞扫描

    20155207 EXP6 信息搜集与漏洞扫描 基础问题回答 1)哪些组织负责DNS,IP的管理. ICANN统一管理全球根服务器 全球根域名服务器(13台) 地区性注册机构(5个)ARIN RIPE ...

  4. 20155323刘威良 网络对抗 Exp2 后门原理与实践

    20155323 刘威良<网络攻防>Exp2后门原理与实践 实验内容 (1)使用netcat获取主机操作Shell,cron启动 (0.5分) (2)使用socat获取主机操作Shell, ...

  5. 汇编 REPNE/REPNZ指令,SCASW指令,SCASD指令,SCAS指令

    知识点: REPNE/REPNZ 指令 SCASW 指令 SCASD 指令 SCAS 指令 一.SCASW 指令 //SCASB cmp word ptr [edi],al //对标志位的影响 ...

  6. 蒙特卡罗方法 python 实现2

    如果不考虑作图,这里的两个例子可以改写成下面的样子: 求圆周率 import random ''' 蒙特卡罗模拟 投点法计算圆周率 ''' # 投点游戏 def play_game(): # 圆 r ...

  7. HTML基础之DOM操作

    DOM(Document Object Model 文档对象模型) 一个web页面的展示,是由html标签组合成的一个页面,dom对象实际就是将html标签转换成了一个文档对象.可以通过dom对象中j ...

  8. LeetCode Search for a Range (二分查找)

    题意 Given a sorted array of integers, find the starting and ending position of a given target value. ...

  9. 关于几个vcenter的合并心得!

    由于最近公司领导觉得公司几个vcenter管理的不同网段IP,办公的时候操作不方便,故本人把几个vcenter上面的esxi主机合并到一个里面! 通过几次测试,发现最简单的方法就是直接用一台vcent ...

  10. Network Mapper 嗅探工具

    1. nmap (目标ip地址 xxx.xxx.xxx.xxx) - 例子:nmap xxx.xxx.xxx.xxx2. nmap自定义扫描 - 例子:nmap -p(端口号) xxx.xxx.xxx ...