文章目录
- 前言
- shiro 核心
- 项目构建
- 默认Session模式
- 配置
- 测试接口
- Realm编写
- 权限测试
- 无权限测试
- 登录测试
- 权限测试
- 前后端分离token
- JWTFilter
- 重写认证
- 修改配置
- 总结
前言
交替换个脑子,一直搞考研的东西,实在是无聊。所以顺便把工程上的东西,拿来遛一遛。你问我,为啥不是机器学习,深度学习,那玩意搞起来头更大,累了。权当是打游戏放松了,那么废话不多说,这里要玩玩的是Shiro,其实一开始我还是喜欢玩这个Security,不过后来,经常用这个人人开源,也就接触这个玩意了,说实话,先前用那个玩意的时候,也是习惯性的把shiro改成security,但是实话实说,太麻烦了,懒得改,所以的话,干脆就是直接使用这个Shiro。
当然关于权限验证,其实我们自己基于RBAC权限管理模型直接做一套都是可以的,基于Spring的AOP,快速做一个简单的这个是非常快的。包括,当初我写的那个WhitHoleV0.7版本其实那个用户端的权限验证都是自己做的。ok,说多了,我们来快速开始吧。
当然自己动手实现一个权限验证其实也不难,shiro只是提供了一个架子而已。后面有时间的话,我们可以直接自己写一个Shiro lite 或者security lite拿过来玩玩。
shiro 核心
ok,我们开始,首先的话,这个shiro由如下模块组成:
Authentication:身份认证/登录,验证用户是不是拥有相应的身份
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限
Session
Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境,也可以是Web
环境的Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储
Web Support:Web 支持,可以非常容易的集成到Web 环境
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率
Concurrency:Shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去
Testing:提供测试支持
“Run As”:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
从我们的使用角度来看,它的运行流程大致如下:
也是分为几个部分:
- subject:这个是对User信息的一些封装
- SecurityManager: 里面实现了对用户信息授权,认证的一些操作
- Realm: 和数据库打交道,比如验证用户权限,这个我们需要查表,那么这个时候,我们就需要这个玩意
也就是说,subject过来之后,通过Manager,去执行对于的执行权限的方法,在进行用户验证的时候,将使用到Realm,去读取数据,之后完成操作。
项目构建
默认Session模式
现在虽然比较流行的是这个前后端分离架构,用的是token,但是,很久以前,还没有分离的时候,还是用的这个,
那么在这边进行整合的时候是这样的:
然后我们导入一下,配置,我这里的话,还导入了这个web starter。这里做演示,我就不建表了。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入shrio-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
配置
那么我们先来看到配置,看看我们看到了流程图,我们其实发现,就是说,我们的请求其实是首先到了一个过滤器,然后在这个过滤器里面进行操作,拦截的,完成权限的认证的。然后,刚刚也说到,完成认证是这样的:
- 拦截到请求
- 进入到安全管理器
- 管理器负责调度对应的认证,其中我们要使用到Realm,去完成这个从数据库,或者说是认证的具体实现。
- 然后就是责任链一路放行,比如验证成功,一路放行到资源,如果失败,就怎么怎么样,这里面有一套操作,我们通过传入到下一层的状态,来判断当前的处理器,要不要处理,然后一条链路走下来,直到走完,或者提前结束。
那么其实都说到这里了,没有接触过Shiro但是,项目写多了的朋友,都看到这个份上了,估计手写一个dome都可以了(真的!)当然,里面还是有很多的一些细节不一样是吧,但是大体大致的一定可以写出来了。
所以,首先,我们要用,就需要先写个Realm.
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/**
* 自定义Realm
*/
public class CustomerRealm extends AuthorizingRealm {
//实现授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//实现认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}
然后呢,我们还要写过Config,给到容器,这里的话,我们使用了这个shiro-starter。所以写完Config之后的话,我们可以就是说可以和SpringBoot一起启动,或者一起注入到Servlet里面,完成运行。
package com.huterox.shirodome.config;
import com.huterox.shirodome.Shiro.CustomerRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
//ShiroFilter过滤所有请求
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给ShiroFilter配置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//配置那些需要放行,需要拦截,需要怎么怎么样
/*这里有对应的注解
*anon:无需认证就可以访问
*authc:必须认证了才能让问
*user:必须拥有记住我功能才能用
*perms:拥有对某个资源的权限才能访问[角色:操作];
*roLe:拥有某个角色权限才能访问[角色]
* @RequiresAuthentication:必须经过认证才能访问
@RequiresUser:必须经过认证,并且有记住我功能才能访问
@RequiresPermissions("permission:operation"):需要拥有指定权限才能访问,其中permission为资源名,operation为操作名
@RequiresRoles("roleName"):需要拥有指定角色(roleName)才能访问
* */
Map<String, String> map = new HashMap<String, String>();
map.put("/hello","anon");
map.put("/admin","authc");
// map.put("/superAdmin","perms[s:p]");
//去登陆接口,没有通过验证进入
shiroFilterFactoryBean.setLoginUrl("toLogin");
shiroFilterFactoryBean.setUnauthorizedUrl("/noauthor");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//创建安全管理器
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}
//创建自定义Realm
@Bean
public Realm getRealm() {
CustomerRealm realm = new CustomerRealm();
return realm;
}
// 对Shiro注解的支持
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
测试接口
ok,那么看完了这个,我们来看到,我们这边准备了那些接口。
package com.huterox.shirodome.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ShiroHelloController {
@RequestMapping("/hello")
public String hello(){
return "Hello";
}
@RequestMapping("/admin")
public String admin(){
return "Admin";
}
@RequestMapping("/toLogin")
public String toLogin(){
return "toLogin";
}
@RequestMapping("/noauthor")
public String noauthor(){
return "木有权限";
}
@RequestMapping("/superAdmin")
@RequiresPermissions("s:p")
public String SP(){
return "高贵的SP你好";
}
@RequestMapping("/login")
public String login(String username,String password){
//获取到用户对象,并且封装起来,方便后面shiro使用
Subject subject = SecurityUtils.getSubject();
// 如果这里还要采用md5”加密“的话
// String salt= "Huterox";
// String passwordSalt = new SimpleHash("MD5", password, salt, 2).toString();
// UsernamePasswordToken token = new UsernamePasswordToken(username,passwordSalt);
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
try {
subject.login(token);
System.out.println("登录成功!!!");
return "OK";
} catch (UnknownAccountException e) {
System.out.println("用户错误!!!");
} catch (IncorrectCredentialsException e) {
System.out.println("密码错误!!!");
}
return "NO";
}
}
因为我们这边是在做认证和授权,所以的话,我们这边有,公共接口,登录接口,拥有特殊授权才能访问的接口。当然还有未授权返回的接口。
Realm编写
在我们的这个Shiro当中,最重要的其实就是这个玩意的实现,在这里,我们要完成的是用户的Authentication和Authorization。
在这里的话,我们这边有两个角色,一个是admin,还有一个是superAdmin。密码都是admin。其他的话,在代码里面有很详细的注释:
package com.huterox.shirodome.Shiro;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/**
* 自定义Realm
*/
public class CustomerRealm extends AuthorizingRealm {
//实现授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权当中");
String userName = (String) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if(userName.equals("superAdmin")){
//只有SuperAdmin才有S:P权限
info.addStringPermission("s:p");
//添加角色也可以
// info.addRole("s");
}else {
info.addStringPermission("");
}
return info;
}
//实现认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("认证当中");
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
//注意,这里假设的是查表得到的username,password,可能是加密了的
String username = "admin";
String password = "admin";
if (token.getUsername().equals(username) || token.getUsername().equals("superAdmin")) {
//这里完成密码匹配,内部会进行处理,一般情况下,获取到的password是明文,或者“自欺欺人”前端对称加密后的东西
//所以,在controller里面,我们要在加密一下,然后,和这里从数据库里面的password进行对比
//ByteSource credentialsSalt = ByteSource.Util.bytes("Huterox");//上面添加账号时候生成的加密盐
//SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username,password,credentialsSalt, getName());
//这里我把token.getUsername()传入进去了,实际上,你可以传入任何对象,然后,接下来在授权部分获取到,这个玩意
//进入下一步的解析
return new SimpleAuthenticationInfo(token.getUsername(),password,"");
}
return null;
}
}
权限测试
ok,现在我们的dome,代码写完了,那么接下来,我们要来看看这个具体的执行过程吧。
无权限测试
首先我们来看到的是第一个接口。
@RequestMapping("/hello")
public String hello(){
return "Hello";
}
在配置里面,我们写了这个:
这个玩意是不需要权限的,所以,此时我们进行一个访问:
一切正常。
登录测试
那么现在,我们来访问admin接口,这个接口,是需要登录才能访问的,现在不登录,进行访问。
此时发现这里需要进入登录页面。这里我没有写html,懒得写了,就给了个提示。
在这里,我们配置了没有登录要调用的接口,和没有授权,或者权限不够要调用的接口
现在,我们登录一下:
登录成功,那么接下来,我们再访问一下:
可以看到成功。
权限测试
ok,接下来,我们来看到权限测试。
现在我们去访问需要超级管理员才能访问的页面。
这里报错了,在终端可以看到是没有权限的错:
这里需要注意的是,我在接口处使用的是注解模式,如果你是在配置里面写好了:
那么就可以跳转到/noauthor里面。
这个时候,我们就需要使用到全局异常处理器了,拦截这些Controller的错误,这里还是在Session模式下,还不是用token的,也就是前后端分离的,所以这里这样很正常。
现在登录超级管理员:
可以看到一切正常:
前后端分离token
现在我们来用用前后端分离的,这里的话,我们需要做的就是结合jwt,然后进行处理了,操作和security是类似的,其实。
我们要做的其实就是在基础上集成JWT。
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
然后的话,我们修改一下过滤器。
这里JWT是啥,怎么用,后面怎么用我就不说了,这个需要结合你实际的项目,而且默认你是有基础的,只是想要玩玩shiro,而已。
JWTFilter
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
// 如果请求头带有token,则对token进行检查;否则,直接放行
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// 判断请求头是否带有 token
if (isLoginAttempt(request, response)) {
// 如果存在 token ,则进入executeLogin()方法执行登入,并检测 token 的正确性
try {
executeLogin(request, response);
} catch (Exception e) {
log.error("Error! {}", e.getMessage());
responseError(response, e.getMessage());
}
}
// 如果不存在 token ,则可能是执行登录操作/游客访问状态,所以直接放行
return true;
}
// 检测 header中是否包含 token
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
return getTokenFromRequest(request) != null;
}
// 执行登入操作
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
String token = getTokenFromRequest(request);
JwtToken jwtToken = new JwtToken(token);
// 提交给 realm 进行登入,如果错误,会抛出异常并捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常,则代表登入成功,返回 true
return true;
}
// 从请求中获取 token
private String getTokenFromRequest(ServletRequest request) {
HttpServletRequest req = (HttpServletRequest) request;
return req.getHeader("Token");
}
// 非法请求将跳转到 "/unauthorized/**"
private void responseError(ServletResponse response, String message) {
try {
HttpServletResponse resp = (HttpServletResponse) response;
// 设置编码,否则中文字符在重定向时会变为空字符串
message = URLEncoder.encode(message, "UTF-8");
resp.sendRedirect("/noauthori/" + message);
} catch (UnsupportedEncodingException e) {
log.error("Error! {}", e.getMessage());
} catch (IOException e) {
log.error("Error! {}", e.getMessage());
}
}
}
这里的话,我们还可以再对JwtToken封装一下,方便后面拿东西。
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
重写认证
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 这里的 token从 JWTFilter 的 executeLogin() 方法传递过来,先前我们封装了jwttoken
//如果验证通过,我们把这个JwtToken往下传递了
String token = (String) authenticationToken.getCredentials();
//然后这里还是查表那一套
return new SimpleAuthenticationInfo(token, token, getName());
}
同样的授权也是一样的。
那么之后的话,我们的流程就是,登录完之后,前端拿到token,我们设置需要验证的地方,就会通过我们的过滤器,然后执行这一套逻辑。
修改配置
最后我们重新修改配置:
@Configuration
public class ShiroConfig {
//ShiroFilter过滤所有请求
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给ShiroFilter配置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//配置那些需要放行,需要拦截,需要怎么怎么样
/*这里有对应的注解
*anon:无需认证就可以访问
*authc:必须认证了才能让问
*user:必须拥有记住我功能才能用
*perms:拥有对某个资源的权限才能访问[角色:操作];
*roLe:拥有某个角色权限才能访问[角色]
* @RequiresAuthentication:必须经过认证才能访问
@RequiresUser:必须经过认证,并且有记住我功能才能访问
@RequiresPermissions("permission:operation"):需要拥有指定权限才能访问,其中permission为资源名,operation为操作名
@RequiresRoles("roleName"):需要拥有指定角色(roleName)才能访问
* */
// 设置自定义的拦截器
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
Map<String, String> map = new HashMap<String, String>();
map.put("/hello","anon");
map.put("/admin","authc");
// map.put("/superAdmin","perms[s:p]");
//去登陆接口,没有通过验证进入
shiroFilterFactoryBean.setLoginUrl("/toLogin");
shiroFilterFactoryBean.setUnauthorizedUrl("/noauthor");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//创建安全管理器
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
// 关闭 shiro 自带的 session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
evaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(evaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
//创建自定义Realm
@Bean
public Realm getRealm() {
CustomerRealm realm = new CustomerRealm();
return realm;
}
// 对Shiro注解的支持
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
总结
okey,这些就是全部内容了。没啥东西其实,就是简单换个脑子,过过。