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. go多种uuid生成方式

    package main import ( "fmt" "github.com/chilts/sid" "github.com/kjk/betterg ...

  2. linux --自已的域名无法登陆机器的解决办法:同步时间

    昨天发现自己的域名无法访问host了,因此我们测试环境便无法安装,显示SSH not connectted ,随后发现时间不同步: 因此以下命令可以实现时间同步: /opt/quest/bin/vas ...

  3. layui的layer.msg,form提交

    layer.msg('提示文字',{shift:1,time:2000},function(){ location.reload();});其中shift是提示框的动画样式,-1为无动画form.on ...

  4. flex布局学习总结--阮一峰

    基本概念:   采用 Flex 布局的元素,称为 Flex 容器(flex container),简称"容器".它的所有子元素自动成为容器成员,称为 Flex 项目(flex it ...

  5. mybatis多对一与一对多

    步骤: 1.创建maven项目 2.编写工具类 3.编写实体类 4.编写mapper接口 5.配置xml 6.测试 多对一:多个学生关联一个老师 工具类: //sqlSessionFactory -- ...

  6. hdu3397 Sequence operation 线段树

    hdu3397 Sequence operation #include <bits/stdc++.h> using namespace std; ; struct node { /// l ...

  7. P2756 飞行员配对方案问题 网络流

    P2756 飞行员配对方案问题 #include <bits/stdc++.h> using namespace std; , inf = 0x3f3f3f; struct Edge { ...

  8. Java基础之数据类型

    一.数据类型 基本数据类型介绍 byte 1字节 char 2字节 short 2字节 int 4字节 long 8字节 float 4字节 double 8字节 以上有Java中八大基本类型的7种, ...

  9. Spring 框架的 AOP 简介

    Spring 框架的 AOP Spring 框架的一个关键组件是面向方面的编程(AOP)(也称为面向切面编程)框架. 面向方面的编程需要把程序逻辑分解成不同的部分称为所谓的关注点. 跨一个应用程序的多 ...

  10. 为什么Tableviewcell创建时可以不判空

    dequeueReuseableCellWithIdentifier:与dequeueReuseableCellWithIdentifier:forIndexPath:的区别: 前者不必向tableV ...