这篇博客旨在完成Springboot2.x与shiro与JWT的整合,先从Springboot2.x整合shiro开始,再将JWT与Shiro整合,其中JWT部分用到了缓存Redis。
shiro介绍网上已经很多了,我们这里引用一下其中一个博客 Shiro框架简介:
Apache Shiro是Java的一个安全框架。对比另一个安全框架Spring Sercurity,它更简单和灵活。Shiro可以帮助我们完成:认证、授权、加密、会话管理、Web集成、缓存等。
- Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
- Authorization:授权,即验证权限,验证某个已认证的用户是否拥有某个权限; 即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限。
- Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通的JavaSE环境的,也可以是Web环境的。
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
- Web Support:Web支持,可以非常容易的集成到Web环境中
- Caching:缓存,比如用户登录后,其用户信息,拥有的角色/权限不必每次去查,提高效率。
- Concurrency:Shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
- Testing:提供测试支持
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问
- Rember Me:记住我,这是非常常见的功能,即一次登录后,下次再来的话不用登录了。
我们看完基本的介绍后,要首先了解一下shiro是怎么工作的:
所以我们只需要完成::
首先我们需要引入pom文件:
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.4.0version>
dependency>
我们引入后需要进行配置,也就是原理中提到的 Subject->SecurityManager->Realm,我们记得顺序。然后开始编写 ShiroConfig 类:
/**
* @Author: goodtimp
* @Date: 2019/10/7 20:46
* @description : shiro配置类
*/
@Configuration
public class ShiroConfig {
/**
* 创建ShiroFilterFactoryBean, 在这里我们可以写入我们的拦截器,例如我们做权限验证的 SecurityManager
*/
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager securityManager){
return null; // 我们等下再来完成这里的内容
}
/**
* 创建DefaultWebSecurityManager 就是SecurityManager
*/
@Bean(name="securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm")ShiroRealm shiroRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联realm
securityManager.setRealm(shiroRealm);
return securityManager;
}
/**
* 创建Realm 这里需要我们自己完成UserRealm的编写,即用户信息获取、权限获取
*/
@Bean(name="userRealm")
public ShiroRealm getRealm(){
return new ShiroRealm();
}
}
下面我们来编写ShiroRealm类,这个类是用于用户信息获取、权限获取,至于 认证 shiro会帮我们完成。
注意: 密码验证时有一个坑点在下面会提到。
/**
* @Author: goodtimp
* @Date: 2019/10/7 20:46
* @description : UserRealm类
*/
public class ShiroRealm extends AuthorizingRealm {
// 没必要提供user表信息,这个根据大家需求自己定吧
@Autowired
private UserService userServiceImpl;
/**
* 获取用户密码
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
String username = (String) usernamePasswordToken.getPrincipal(); // 获取username 当然也可能是phone信息,这要看你传过来的是什么
String password = new String((char[]) (usernamePasswordToken.getCredentials())); // 无法通过.toString转换 这个密码是我们传过来的密码,明文原密码,但是数据库中的密码是加密后的密码,所以下面还需要处理
if (StringUtils.isEmpty(username)) {
throw new AccountException("用户名不能为空");
}
if (StringUtils.isEmpty(password)) {
throw new AccountException("密码不能为空");
}
User user = userServiceImpl.getUserByName(username); // 获取数据库中的密码 此处密码已经 加盐加密
// user.getUserPassword() 应该是数据库对应的加密后的密码字段
return new SimpleAuthenticationInfo(user, user.getUserPassword(), ByteSource.Util.bytes(user.getSalt()), getName());
// 四个参数的意思为: 数据库中取出的用户主体信息, 用户数据库中密码,加密盐,类名(这个是自己可以点进去看)
// 记录:至于怎么验证 下面会说到
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
String userId = JwtUtil.getClaim(principalCollection.toString(), PAYLOAD_USER_TAG.getCode());
// 从redis中获取角色对应的权限数据
// ------- code --------
// 将权限字符串添加到这里即可
simpleAuthorizationInfo.addStringPermission("user:test");
return simpleAuthorizationInfo;
}
}
我们上面将数据库中传进来的密码, 加盐加密验证不通过的问题,在跟踪源码之后发现 shiro 在加密时用的方法不太一样。如果你也碰到了这个问题 可以去看一下我的另外一篇博客:shiro 加盐加密问题
上面在ShiroConfig类中我们做了标记,这个类是作为拦截器使用的,所有的请求都会被他拦截并验证是否需要进行安全控制。
/**
* Shiro内置过滤器,可以实现权限相关的拦截器
* 常用的过滤器:
* anon: 无需认证(登录)可以访问
* authc: 必须认证才可以访问
* user: 如果使用rememberMe的功能可以直接访问
* perms: 该资源必须得到资源权限才可以访问
* role: 该资源必须得到角色权限才可以访问
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager); //设置安全管理器
// shiroFilterFactoryBean.setLoginUrl("/login");// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
// shiroFilterFactoryBean.setSuccessUrl("/index"); // 登录成功后要跳转的链接
// shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth");//未授权界面;
// Map filterMap = new HashMap();
// filterMap.put("/js/**","anon");
// filterMap.put("/html/**","anon");
// filterMap.put("/login/index","authc");
// filterMap.put("/login/login","anon");
// filterMap.put("/logout","logout");//配置退出 过滤器,其中的具体的退出代码Shiro已经实现
// filterMap.put("/**","authc");//过滤链定义,从上向下顺序执行,一般将/**放在最为下边
// shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
这个类我不想多说什么…注释很多了
@GetMapping("/login")
@ResponseBody
public ResponseModel login(String name, String password) {
/**
* 使用Shiro编写认证操作
*/
//1.获取Subject
Subject subject = SecurityUtils.getSubject();
//2.封装用户数据
UsernamePasswordToken token = new UsernamePasswordToken(name, password);
//3.执行登录方法
try {
subject.login(token); // 如果不报错则成功
return ResponseModel.success("token", token);
} catch (AccountException e) {
return ResponseModel.fail(e.getMessage());
} catch (IncorrectCredentialsException e) {
return ResponseModel.fail("Shiro密码错误");
}
}
感兴趣的可以自己去调试一下…
那我们怎么控制用户访问某个接口必须具有的的权限呢,shiro为我们提供了一个非常方便的注解:@RequiresPermissions(“user:test”)
使用:
@RequiresPermissions("user:test")
public String hello() {
return "hello world!";
}
这样用户必须要有user:test权限才能访问这个接口,而权限配置则在文章中间的 ShiroRealm类中写了。
权限注解使用: @RequiresPermissions 解释
本来这篇文章想把shiro jwt整合放在一起写的,后来觉得东西太多所以还是分开吧。这里放上shiro整合jwt的博文链接,欢迎各位做前后端分离的小伙伴前来观看 springboot2.x shiro Jwt 整合。
本文有什么不对的地方还请在评论中指出。