前言
最后调用过程参考了 :
https://blog.csdn.net/long270022471/article/details/62423286
最近可能要用到,所以看了看shiro.
结合人人开源的管理后端代码以及其他大佬的写的有关shiro的博文,自己整理了一下避免以后忘记了.
其中代码中我加了很多注释,是个人的通俗理解, 可能不正确, 也欢迎大家指出共同进步.
Shiro的功能
- Authentication:身份认证,验证用户是否拥有某个身份。
- Authorization: 权限校验,验证某个已认证的用户是否拥有某个权限。确定“谁”可以访问“什么”。
- Session Management:会话管理,管理用户登录后的会话,
- Cryptography:加密,使用密码学加密数据,如加密密码。
- Web Support:Web支持,能够比较轻易地整合到Web环境中。
- Caching:缓存,对用户的数据进行缓存,
- Concurrency:并发,Apache Shiro支持具有并发功能的多线程应用程序,也就是说支持在多线程应用中并发验证。
- Testing:测试,提供了测试的支持。
- Run as :允许用户以其他用户的身份来登录。
- Remember me :记住我
依赖包
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
简单实现
共需要5个文件
CustomRealm: 自定义的认证源, 身份认证和权限认证
OAuthToken: token实体类
ShiroConfig: shiro的一些配置,相对于之前的xml配置。包括:过滤的文件和权限,密码加密的算法,其用注解等相关功能
TestFilter 自定义过滤器
TokenGenerator token生成器, 测试功能流程的话可以不用这个, 实际中生成token的逻辑
自定义令牌实体类
package com.atguigu.gulimall.shiro_demo.config;
import org.apache.shiro.authc.AuthenticationToken;
/**
* @author zhangshaokun
* @date 2021/3/11
* 自定义令牌实体类
*/
public class OAuthToken implements AuthenticationToken {
private String token;
public OAuthToken(String token){
this.token = token;
}
@Override
public String getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
token生成器
package com.atguigu.gulimall.shiro_demo.demo;
import java.security.MessageDigest;
import java.util.UUID;
/**
* 生成token
*
*/
public class TokenGenerator {
public static String generateValue() {
return generateValue(UUID.randomUUID().toString());
}
private static final char[] hexCode = "0123456789abcdef".toCharArray();
public static String toHexString(byte[] data) {
if(data == null) {
return null;
}
StringBuilder r = new StringBuilder(data.length*2);
for ( byte b : data) {
r.append(hexCode[(b >> 4) & 0xF]);
r.append(hexCode[(b & 0xF)]);
}
return r.toString();
}
public static String generateValue(String param) {
try {
MessageDigest algorithm = MessageDigest.getInstance("MD5");
algorithm.reset();
algorithm.update(param.getBytes());
byte[] messageDigest = algorithm.digest();
return toHexString(messageDigest);
} catch (Exception e) {
// 这个可具体更改,现在做例子不动了,可以自定义异常类进行抛出提醒
throw new RuntimeException("生成Token失败", e);
}
}
}
CustomRealm类
自定义认证源
package com.atguigu.gulimall.shiro_demo.demo;
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;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* @author zhangshaokun
* @date 2021/3/11
*/
@Component
public class CustomRealm extends AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof OAuthToken;
}
/**
* 权限认证,即登录过后,每个身份不一定,对应的所能看的页面也不一样
* */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//这个里面存储的是主要身份信息,来源便是下面身份认证最后存入的第一个参数
Object primaryPrincipal = principalCollection.getPrimaryPrincipal();
//利用这些信息,便能从数据库或者其他位置获取权限列表了,具体实现略
//获取用户的权限列表,存入set中
Set<String> permsSet = new HashSet<>();
//授权信息实例
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//传入一个set
info.setStringPermissions(permsSet);
return info;
}
/**
* 身份认证。即登录通过账号和密码验证登陆人的身份信息
* */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取token
String token = (String) authenticationToken.getPrincipal();
//通过token获取用户信息, 比如token和用户信息存储在redis或数据库, 那么便根据token去查询
//这里用map模拟数据库
Map<String, String> token_value = new HashMap<>();
token_value.put("6110e95350b2af23be9c70b93d0c118f","张三");
token_value.put("token2","李四");
//判断是否读取到信息
String value = token_value.get(token);
//如果没有查询到信息,或者token设置的时间已经过期
if (StringUtils.isEmpty(value)){
System.out.println("token失效,请重新登录");
throw new IncorrectCredentialsException("token失效,请重新登录");
}
//token有效, 就查询用户的详细信息
//还可以在做一些判断,比如用户有没有被注销,被封禁等等
//具体实现略
String str = ""; //这个只是临时传参避免报错的
// 将用户信息(具体类型看需求,可以是简单的string也可以是对象,要方便上面权限认证是容易获取用户信息), token 传入
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(str, token, getName());
return info;
}
}
ShiroConfig类
package com.atguigu.gulimall.shiro_demo.demo;
import org.apache.shiro.mgt.SecurityManager;
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 javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author zhangshaokun
* @date 2021/3/11
*/
@Configuration
public class ShiroConfig {
/*随记: 根据三个方法的引用顺序排了序*/
/*这里CustomRealm类没有加注解注册到spring框架,所以方法new出来返回的,如果类上加了,这里可以省略这个方法
* 比如给CustomRealm类用Component注解进行注册后, 直接向securityManager(CustomRealm customRealm)方法中传参即可,如下*/
/* @Bean
public CustomRealm customRealm() {
CustomRealm customRealm = new CustomRealm();
return customRealm;
}*/
/*SecurityManger:SecurityManager是Shiro核心,主要协调Shiro内部的各种安全组件,这个我们不需要太关注,只需要知道可以设置自定的Realm*/
@Bean("securityManager")
public SecurityManager securityManager(CustomRealm customRealm) {
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
//存入Realm
defaultSecurityManager.setRealm(customRealm);
return defaultSecurityManager;
}
/*bean名称必须是shiroFilter*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
//创建一个shiroFilter实例
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
//配置Manager
shiroFilter.setSecurityManager(securityManager);
//自定义过滤规则,可以自定义多个, 通过map传入
Map<String, Filter> filters = new HashMap<>();
//第一个参数过滤器, 第二个参数是具体的过滤器实例
filters.put("oauth2", new TestFilter());
//将自定义的过滤器加入shiroFilter中
shiroFilter.setFilters(filters);
//设置登录路径,设置后登录路径下不走认证,也可以用下面kv的形式写入
shiroFilter.setLoginUrl("/login");
//设置权限不足是跳转页面
shiroFilter.setUnauthorizedUrl("/notRole");
//上面两个方法不能很全面,所以可以通过LinkedHashMap向过滤器传入过滤配置, 因为存入的规则是有序的
Map<String, String> filterMap = new LinkedHashMap<>();
//第一个参数是路径, 第二个参数字典值,标识过滤规则
//authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
//需要注意匹配按照上下顺序来的, 如果/**写在最前面那么,后面的都被覆盖了
filterMap.put("/sys/login","anon"); //这个的意思就是 /sys/login 可以匿名访问不进行认证
filterMap.put("/swagger/**", "anon"); //可以用**进行匹配所有子路径
filterMap.put("/swagger-ui.html", "anon"); //也可以是静态文件
filterMap.put("/admin/**", "authc"); //admin下所有都必须通过认证才能访问
filterMap.put("/test/**", "oauth2"); //还可以指定自定义的过滤规则
filterMap.put("/**", "oauth2");
//将上面配置的kv存入
shiroFilter.setFilterChainDefinitionMap(filterMap);
//返回
return shiroFilter;
}
}
filter类
package com.atguigu.gulimall.shiro_demo.demo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* @author zhangshaokun
* @date 2021/3/11
* 自定义的过滤器
*/
public class TestFilter extends AuthenticatingFilter{
@Override
protected AuthenticationToken createToken(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws Exception {
//获取请求token
String token = getRequestToken((HttpServletRequest) servletRequest);
//如果携带没有携带token,则返回null
if(StringUtils.isEmpty(token)){
return null;
}
//携带了token便返回自定义的token实例
return new OAuthToken(token);
}
@Override
protected boolean onAccessDenied(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws Exception {
//获取请求token
String token = getRequestToken((HttpServletRequest) servletRequest);
//如果不存在返回401, 这里简化为输出,实际中要给前端返回401及其他错误信息
if(StringUtils.isEmpty(token)){
System.out.println("错误401:token为空");
return false;
}
return executeLogin(servletRequest, servletResponse);
}
/**
* 获取请求中的token
*/
private String getRequestToken(HttpServletRequest httpRequest){
//从header中获取token
String token = httpRequest.getHeader("token");
//如果header中不存在token,则从参数中获取token
if(StringUtils.isEmpty(token)){
token = httpRequest.getParameter("token");
}
return token;
}
}
自定义认证时流程
参考链接: https://blog.csdn.net/long270022471/article/details/62423286
这里只是方便自己理解, 那些个文件是干啥的用在了哪…
1.首先根据config中路径匹配到对应的自定义filter
2.filter中首先进入onAccessDenied方法,判断请求有没有token
3.存在token时便进入executeLogin()进行登录步骤 ,其中会调用filter中的重写的createToken方法获取token实例, 调用subject.login(token)进行具体登录
4.跟随参考链接中的具体流程走回到这一步,用到了我们在Realm中重写的supports方法
5.认证这一步深入便会进入重写的身份认证方法
测试
建一个controller测试
@RestController
@RequestMapping("/sys")
public class testController {
//模拟登陆接口,不需要经过认证
@RequestMapping("/login")
public String login(){
//实际中肯定要把token返回的, 因为我的验证中token是写死的所以偷懒了
return new String("登陆成功");
}
//模拟需要认证的接口
@RequestMapping("/query")
public String querytest(){
return new String("查询成功");
}
}
然后用postman调借借口就可以了
http://localhost:10000/sys/login 因为配置的不走认证所以不加token也可以调用
http://localhost:10000/sys/query 不加token 会输出错误信息,因为我懒,没往前端返所以只能在控制台看
加token便可以调用成功