Shiro (Shiro + JWT + SpringBoot应用)

1.Shiro的简介

Apache Shiro是一种功能强大且易于使用的Java安全框架,它执行身份验证,授权,加密和会话管理,可用于保护 从命令行应用程序,移动应用程序到Web和企业应用程序等应用的安全。

  1. Authentication 身份认证/登录,验证用户是不是拥有相应的身份;
  2. Authorization 授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
  3. Cryptography 安全数据加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
  4. Session Management 会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;
  5. Web Integration web系统集成
  6. Interations 集成其它应用,spring、缓存框架

从应用程序角度的来观察如何使用Shiro完成工作:

Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;

SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;

Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

也就是说对于我们而言,最简单的一个Shiro应用:

1、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;

2、我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。

2.Shiro + JWT + SpringBoot

1.导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.2</version>
</dependency>
2.配置JWT
public class JWTUtil {
/**
* 校验 token是否正确
*
* @param token 密钥
* @param secret 用户的密码
* @return 是否正确
*/
public static boolean verify(String token, String username, String secret) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
verifier.verify(token);
return true;
} catch (Exception e) {
log.info("token is invalid{}", e.getMessage());
return false;
}
} public static String getUsername(HttpServletRequest request) {
// 取token
String token = request.getHeader("Authorization");
return getUsername(UofferUtil.decryptToken(token));
}
/**
* 从 token中获取用户名
* @return token中包含的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
log.error("error:{}", e.getMessage());
return null;
}
} public static Integer getUserId(HttpServletRequest request) {
// 取token
String token = request.getHeader("Authorization");
return getUserId(UofferUtil.decryptToken(token));
}
/**
* 从 token中获取用户ID
* @return token中包含的ID
*/
public static Integer getUserId(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return Integer.valueOf(jwt.getSubject());
} catch (JWTDecodeException e) {
log.error("error:{}", e.getMessage());
return null;
}
} /**
* 生成 token
* @param username 用户名
* @param secret 用户的密码
* @return token 加密的token
*/
public static String sign(String username, String secret, Integer userId) {
try {
Map<String, Object> map = new HashMap<>();
map.put("alg", "HS256");
map.put("typ", "JWT");
username = StringUtils.lowerCase(username);
Algorithm algorithm = Algorithm.HMAC256(secret);
return JWT.create()
.withHeader(map)
.withClaim("username", username)
.withSubject(String.valueOf(userId))
.withIssuedAt(new Date())
// .withExpiresAt(date)
.sign(algorithm);
} catch (Exception e) {
log.error("error:{}", e);
return null;
}
}
}
3.配置Shiro
4.实现JWTToken

token自己已经包含了用户名等信息。

@Data
public class JWTToken implements AuthenticationToken { private static final long serialVersionUID = 1282057025599826155L; private String token; private String exipreAt; public JWTToken(String token) {
this.token = token;
} public JWTToken(String token, String exipreAt) {
this.token = token;
this.exipreAt = exipreAt;
} @Override
public Object getPrincipal() {
return token;
} @Override
public Object getCredentials() {
return token;
} }
5.实现Realm

自定义实现 ShiroRealm,包含认证和授权两大模块。

public class ShiroRealm extends AuthorizingRealm {

    @Resource
private RedisUtil redisUtil; @Autowired
private ISysUserService userService; @Autowired
private ISysRoleService roleService; @Autowired
private ISysMenuService menuService; // 必须重写此方法,不然Shiro会报错
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
} /**
* 只有当需要检测用户权限的时候才会调用此方法
* 授权模块,获取用户角色和权限。
* @param token token
* @return AuthorizationInfo 权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection token) {
Integer userId = JWTUtil.getUserId(token.toString()); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); // 获取用户角色集
Set<String> roleSet = roleService.selectRolePermissionByUserId(userId);
simpleAuthorizationInfo.setRoles(roleSet); // 获取用户权限集
Set<String> permissionSet = menuService.findUserPermissionsByUserId(userId);
simpleAuthorizationInfo.setStringPermissions(permissionSet);
return simpleAuthorizationInfo;
} /**
* 用户认证:编写shiro判断逻辑,进行用户认证
* @param authenticationToken 身份认证 token
* @return AuthenticationInfo 身份认证信息
* @throws AuthenticationException 认证相关异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 这里的 token是从 JWTFilter 的 executeLogin 方法传递过来的,已经经过了解密
String token = (String) authenticationToken.getCredentials();
String encryptToken = UofferUtil.encryptToken(token); //加密token
String username = JWTUtil.getUsername(token); //从token中获取username
Integer userId = JWTUtil.getUserId(token); //从token中获取userId // 通过redis查看token是否过期
HttpServletRequest request = HttpContextUtil.getHttpServletRequest();
String ip = IPUtil.getIpAddr(request);
String encryptTokenInRedis = redisUtil.get(Constant.RM_TOKEN_CACHE + encryptToken + StringPool.UNDERSCORE + ip);
if (!token.equalsIgnoreCase(UofferUtil.decryptToken(encryptTokenInRedis))) {
throw new AuthenticationException("token已经过期");
} // 如果找不到,说明已经失效
if (StringUtils.isBlank(encryptTokenInRedis)) {
throw new AuthenticationException("token已经过期");
} if (StringUtils.isBlank(username)) {
throw new AuthenticationException("token校验不通过");
} // 通过用户id查询用户信息
SysUser user = userService.getById(userId); if (user == null) {
throw new AuthenticationException("用户名或密码错误");
}
if (!JWTUtil.verify(token, username, user.getPassword())) {
throw new AuthenticationException("token校验不通过");
}
return new SimpleAuthenticationInfo(token, token, "febs_shiro_realm");
}
}
6.重写Filter

所有的请求都会先经过 Filter,所以我们继承官方的 BasicHttpAuthenticationFilter ,并且重写鉴权的方法。

代码的执行流程 preHandle -> isAccessAllowed -> isLoginAttempt -> executeLogin

@Slf4j
public class JWTFilter extends BasicHttpAuthenticationFilter { private static final String TOKEN = "Authorization"; private AntPathMatcher pathMatcher = new AntPathMatcher(); /**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个 option请求,这里我们给 option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
} @Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
UofferProperties UofferProperties = SpringContextUtil.getBean(UofferProperties.class);
// 获取免认证接口 url
// 在application.yml中配置/adminApi/auth/doLogin/**,/adminApi/auth/register/**, ...
String[] anonUrl = StringUtils.splitByWholeSeparatorPreserveAllTokens(UofferProperties.getShiro().getAnonUrl(), ","); boolean match = false;
for (String u : anonUrl) {
if (pathMatcher.match(u, httpServletRequest.getRequestURI())) {
match = true;
}
}
if (match) {
return true;
}
if (isLoginAttempt(request, response)) {
return executeLogin(request, response);
}
return false;
} /**
* 判断用户是否想要登入。
* 检测header里面是否包含Authorization字段即可
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader(TOKEN);
return token != null;
} @Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(TOKEN); //得到token
JWTToken jwtToken = new JWTToken(UofferUtil.decryptToken(token)); // 解密token
try {
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
} catch (Exception e) {
log.error(e.getMessage());
return false;
}
} @Override
protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
log.debug("Authentication required: sending 401 Authentication challenge response.");
HttpServletResponse httpResponse = WebUtils.toHttp(response);
// httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpResponse.setCharacterEncoding("utf-8");
httpResponse.setContentType("application/json; charset=utf-8");
final String message = "未认证,请在前端系统进行认证";
final Integer status = 401;
try (PrintWriter out = httpResponse.getWriter()) {
// String responseJson = "{\"message\":\"" + message + "\"}";
JSONObject responseJson = new JSONObject();
responseJson.put("msg", message);
responseJson.put("status", status);
out.print(responseJson);
} catch (IOException e) {
log.error("sendChallenge error:", e);
}
return false;
} }
7. ShiroConfig
@Configuration
public class ShiroConfig { @Bean
public ShiroRealm shiroRealm() {
// 配置 Realm
return new ShiroRealm();
} // 创建DefaultWebSecurityManager
@Bean("securityManager")
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 配置 SecurityManager,并注入 shiroRealm
securityManager.setRealm(shiroRealm());
return securityManager;
} // 创建ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置 securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager); //添加Shiro过滤器
/**
* Shiro内置过滤器,可以实现权限相关的拦截器
* 常用的过滤器:
* anon: 无需认证(登录)可以访问
* authc: 必须认证才可以访问
* user: 如果使用rememberMe的功能可以直接访问
* perms: 该资源必须得到资源权限才可以访问
* role: 该资源必须得到角色权限才可以访问
*/ // 在 Shiro过滤器链上加入 自定义过滤器JWTFilter 并取名为jwt
LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
filters.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filters); // 自定义url规则
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 所有请求都要经过 jwt过滤器
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
} /**
* 下面的代码是添加注解支持
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
// 设置代理类
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true); return creator;
} /**
* 开启aop注解支持
*
* @param securityManager
* @return
*/
@Bean("authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
} // Shiro生命周期处理器
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
} }
8.登陆
    /**
* 登录方法
*
* @param username 用户名
* @param password 密码
* @param code 验证码
* @param uuid 唯一标识
* @return 结果
*/
@PostMapping("/doLogin")
public ResultVo login(String username, String password, String code, String uuid, HttpServletRequest request) throws UofferException { String verifyKey = Constant.RM_CAPTCHA_CODE_KEY + uuid;
String captcha = redisUtil.getCacheObject(verifyKey);
redisUtil.del(verifyKey); if (captcha == null) {
return ResultVo.failed(201, "验证码失效");
}
if (!code.equalsIgnoreCase(captcha)) {
return ResultVo.failed(201, "验证码错误");
} username = StringUtils.lowerCase(username);
password = MD5Util.encrypt(username, password); final String errorMessage = "用户名或密码错误";
SysUser user = userManager.getUser(username); if (user == null) {
return ResultVo.failed(201, errorMessage);
}
if (!StringUtils.equalsIgnoreCase(user.getPassword(), password)) {
return ResultVo.failed(201, errorMessage);
}
if (Constant.STATUS_LOCK.equals(user.getStatus())) {
return ResultVo.failed(201, "账号已被锁定,请联系管理员!");
} Integer userId = user.getUserId();
String ip = IPUtil.getIpAddr(request);
String address = AddressUtil.getCityInfo(ip);
// 更新用户登录时间
SysUser sysUser = new SysUser();
sysUser.setUserId(userId);
sysUser.setLastLoginTime(new Date());
sysUser.setLastLoginIp(ip);
userService.updateById(sysUser); // 拿到token之后加密
String sign = JWTUtil.sign(username, password, userId);
String token = UofferUtil.encryptToken(sign);
LocalDateTime expireTime = LocalDateTime.now().plusSeconds(properties.getShiro().getJwtTimeOut());
String expireTimeStr = DateUtil.formatFullTime(expireTime);
JWTToken jwtToken = new JWTToken(token, expireTimeStr); // 将登录日志存入日志表中
SysLoginLog loginLog = new SysLoginLog();
loginLog.setIp(ip);
loginLog.setAddress(address);
loginLog.setLoginTime(new Date());
loginLog.setUsername(username);
loginLog.setUserId(userId);
loginLogService.save(loginLog); saveTokenToRedis(username, jwtToken, ip, address);
JSONObject data = new JSONObject();
data.put("Authorization", token); // 将用户配置及权限存入redis中
userManager.loadOneUserRedisCache(userId);
return ResultVo.oK(data);
}
9.@RequiresPermissions

要求subject中必须含有bus:careerTalk:query的权限才能执行方法someMethod()。否则抛出异常AuthorizationException

@RequiresPermissions("bus:careerTalk:query")
public void someMethod() {
}

引用:

https://www.iteye.com/blog/jinnianshilongnian-2018398

https://www.jianshu.com/p/f37f8c295057

Shiro (Shiro + JWT + SpringBoot应用)的更多相关文章

  1. shiro,基于springboot,基于前后端分离,从登录认证到鉴权,从入门到放弃

    这个demo是基于springboot项目的. 名词介绍: ShiroShiro 主要分为 安全认证 和 接口授权 两个部分,其中的核心组件为 Subject. SecurityManager. Re ...

  2. spring jwt springboot RESTful API认证方式

    RESTful API认证方式 一般来讲,对于RESTful API都会有认证(Authentication)和授权(Authorization)过程,保证API的安全性. Authenticatio ...

  3. Shiro Shiro Web Support and EnvironmentLoaderListener

    Shiro Shiro Web Support 主要参考: http://shiro.apache.org/web.html 还有涛哥的 作为资源控制访问的事情,主要使用在网络后台方面,所以了解了本地 ...

  4. 【SpringBoot技术专题】「权限校验专区」Shiro整合JWT授权和认证实现

    本章介绍一下常用的认证框架Shiro结合springboot以及集合jwt快速带您开发完成一个认证框架机制. Maven配置依赖 <dependency> <groupId>o ...

  5. shiro+jwt+springboot理解

    转自 https://www.cnblogs.com/fengli9998/p/6676783.html https://www.jianshu.com/p/0366a1675bb6 https:// ...

  6. SpringBoot + Shiro + shiro.ini 的踩坑记录

    0.写在前面的话 好久没写博客了,诶,好多时候偷懒直接就抓网上的资料丢笔记里了,也就没有自己提炼,偷懒偷懒.然后最近参加了一个网络课程,要交作业的那种,为了能方便看下其他同学的作业,就写了个爬虫把作业 ...

  7. 学习Spring Boot:(十六)使用Shiro与JWT 实现认证服务

    前言 需要把Web应用做成无状态的,即服务器端无状态,就是说服务器端不会存储像会话这种东西,而是每次请求时access_token进行资源访问.这里我们将使用 JWT 1,基于散列的消息认证码,使用一 ...

  8. [Shiro] - Shiro之SpringBoot中的使用

    下载了运行项目后,访问路径:http://localhost/shiro/login 这篇应该在进阶后面的. shiro中的重中之重,一定要看. 基于springboot+thymeleaf+shir ...

  9. Shiro 使用 JWT Token 配置类参考

    项目中使用了 Shiro 进行验证和授权,下面是 Shiro 配置类给予参考. 后来并没有使用 Shiro,感觉使用 JWT 还是自己写拦截器比较灵活,使用 Shiro 后各种地方需要魔改,虽然功能也 ...

随机推荐

  1. Istio的流量管理(概念)(istio 系列二)

    Istio的流量管理(概念) 目录 Istio的流量管理(概念) 概述 Virtual services 为什么使用virtual service Virtual services举例 hosts字段 ...

  2. 【matlab 基础篇 01】快速开始第一个程序(详细图文+文末资源)

    快速入门matlab,系统地整理一遍,如何你和我一样是一个新手,那么此文很适合你: 文章目录 1 软件安装 2 打开软件 3 编写程序 3.1 基础步骤 3.2 添加PATH 3.3 命令行模式 4 ...

  3. [codeforces 200 A Cinema]暴力,优化

    题意大致是这样的:有一个有n行.每行m个格子的矩形,每次往指定格子里填石子,如果指定格子里已经填过了,则找到与其曼哈顿距离最小的格子,然后填进去,有多个的时候依次按x.y从小到大排序然后取最小的.输出 ...

  4. Python哈希表和解析式

    目录 1. 封装和解构 1.1 封装 1.2 解构 2. 集合Set 2.1 初始化 2.2 增加 2.3 删除 2.4 遍历 2.5 并集&交集&差集&对称差集 3.字典 3 ...

  5. hive数据仓库入门到实战及面试

    第一章.hive入门 一.hive入门手册 1.什么是数据仓库 1.1数据仓库概念 对历史数据变化的统计,从而支撑企业的决策.比如:某个商品最近一个月的销量,预判下个月应该销售多少,从而补充多少货源. ...

  6. 接口testing简介

    一.基础介绍 1.什么是接口 我们常说的接口一般指2种1)API:应用程序编程接口 2)GUI:图形用户界面(接口) 这里我们主要说API——接口测试   2.接口测试的目的 测试接口的正确性和稳定性 ...

  7. 视口viewport

    一.viewport 1. 何为视口? 视口是浏览器显示网页的矩形区域. 2. 默认视口:模拟一个大约1000像素宽的视口. 理想视口:因设备.操作系统.浏览器而异,一般而言,手机宽带大约在300-5 ...

  8. ConcurrentHashMap.Segment源码解析

    ConcurrentHashMap通过将完整的表分成若干个segment的方式实现锁分离,每个segment都是一个独立的线程安全的Hash表,当需要操作数据时,HashMap通过Key的hash值和 ...

  9. NodeJS的概述

    1.NodeJS概述 基于谷歌V8引擎,运行在服务器端的环境 对比JS和NodeJS (1)JS运行在浏览器端,存在多种浏览器解释器,容易产生兼容性的问题:而NodeJS运行在服务器端,只有V8引擎一 ...

  10. DOM面试题

    1.利用冒泡和不利用冒泡的差别 答案: 1.绑定位置不同:不利用冒泡绑定在目标元素上,利用冒泡绑定在父元素上. 2.监听对象的个数不同:不利用冒泡会反复创建多个监听,利用冒泡始终只有 一个监听. 3. ...