1.shiro导包
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
<!-- 工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.6</version>
</dependency>
<!-- mybatis-plus基本包 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- mybatis-plus代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!-- mybatis-plus代码生成器 添加模板引擎 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<!---->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!--ehcache缓存-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.5.3</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Token生成与解析-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
2.注意点
-
若同时使用RequiresPermissions和RequiresRoles两个注解,条件是且,不是或
@RequiresPermissions("hello:helloadmin") @RequiresRoles("admin") @GetMapping("/helloAdmin") public String helloAdmin(){ return "hello admin"; }
上面这个代码,一定是要有admin角色,且有hello:helloadmin权限才能访问
-
无法使用权限注解的问题
我使用如下版本,并无此问题
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.5.3</version> </dependency>
若出现了无法使用注解的情况,则在ShiroConfig中加入如下配置
@Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * 下面的代码是添加注解支持 * * @return */ @Bean @DependsOn("lifecycleBeanPostProcessor") public static DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; }
-
在AdminAuthorizingRealm中,doGetAuthenticationInfo返回的SimpleAuthenticationInfo第一个参数是认证主体,网上教程返回的是用户名,但我更喜欢使用当前登录对象ComAdmin admin,这样在获取用户主体时候,可以通过如下代码获得,否则只能获取到用户名:
Subject currentUser = SecurityUtils.getSubject(); ComAdmin admin = (ComAdmin) currentUser.getPrincipal();
getPrincipal() 获取的就是SimpleAuthenticationInfo传入的第一个参数
3.简单集成
只集成基本功能,认证和授权均直接从数据库读取,
3.1 创建表结构(5张)
-
com_admin: 密码为加盐且散列后的数据
CREATE TABLE `com_admin` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(63) DEFAULT NULL, `password` varchar(63) DEFAULT NULL, `salt` varchar(255) DEFAULT NULL COMMENT '盐', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='用户表';
-
com_role
CREATE TABLE `com_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(63) DEFAULT NULL COMMENT '角色名称', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='角色表';
-
com_admin_role
CREATE TABLE `com_admin_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `admin_id` int(11) DEFAULT NULL, `role_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-
com_perms
CREATE TABLE `com_perms` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(127) DEFAULT NULL COMMENT '权限名称', `url` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-
com_role_perms
CREATE TABLE `com_role_perms` ( `id` int(11) NOT NULL AUTO_INCREMENT, `role_id` int(11) DEFAULT NULL, `perms_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
3.2 配置ShiroConfig
import com.example.shirodemo.shiro.CustomerRealm;
import com.example.shirodemo.shiro.cache.RedisCacheManager;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.LinkedHashMap;
import java.util.Map;
/**
* @version 1.0
* @author: hjianfeng
* @date: 2022/4/1 17:12
* 用来整合shiro相关的配置类
*/
@Configuration
public class ShiroConfig {
//1. 创建shiroFilter 负责拦截所有请求,
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 给Filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
// 配置系统受限资源
// 配置系统公共资源
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 一定要放在通配符的上面
filterChainDefinitionMap.put("/auth/nologin", "anon"); // anon 不需要认证和授权, url可以匿名访问
filterChainDefinitionMap.put("/auth/login", "anon");
filterChainDefinitionMap.put("/auth/register", "anon");
filterChainDefinitionMap.put("/**", "authc"); // authc 请求这个资源需要认证和授权
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
// 默认认证界面路径
shiroFilterFactoryBean.setLoginUrl("/auth/nologin");
return shiroFilterFactoryBean;
}
//2. 创建安全管理器
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 给安全管理器设置Realm
securityManager.setRealm(realm);
return securityManager;
}
//3. 创建自定义realm
@Bean
public Realm realm() {
CustomerRealm customerRealm = new CustomerRealm();
// 修改凭证校验匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 设置加密算法为MD5
credentialsMatcher.setHashAlgorithmName("MD5");
// 设置散列次数,必须跟注册一样
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
return customerRealm;
}
}
3.3 自定义Realm
此处用的盐,是处理了序列化和反序列化后的自定义MySimpleByteSource,是为了解决redis反序列化失败后的问题,如不需要redis,可直接使用**SimpleByteSource.Util.bytes(admin.getSalt())**替换
import com.example.shirodemo.pojo.Admin;
import com.example.shirodemo.pojo.Perms;
import com.example.shirodemo.pojo.Role;
import com.example.shirodemo.service.AdminService;
import com.example.shirodemo.service.PermsService;
import com.example.shirodemo.service.RoleService;
import com.example.shirodemo.shiro.salt.MySimpleByteSource;
import com.example.shirodemo.utils.BeanUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
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.util.Assert;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @version 1.0
* @author: hjianfeng
* @date: 2022/4/1 17:23
*/
@Slf4j
public class CustomerRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("================ 授权 ===============");
// 获取身份信息
if (principals == null) {
throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
}
RoleService roleService = (RoleService) BeanUtil.getBean("roleService");
PermsService permsService = (PermsService) BeanUtil.getBean("permsService");
if (principals.getPrimaryPrincipal() instanceof String) {
// 用username当SimpleAuthenticationInfo主身份信息
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
List<Role> roleList = roleService.getRoleByUsername(username);
List<Perms> permsList = permsService.getPermsByUsername(username);
if (!CollectionUtils.isEmpty(roleList)) {
Set<String> roleSet = roleList.stream().map(Role::getName).collect(Collectors.toSet());
simpleAuthorizationInfo.setRoles(roleSet);
}
if (!CollectionUtils.isEmpty(permsList)) {
Set<String> permsSet = permsList.stream().map(Perms::getName).collect(Collectors.toSet());
simpleAuthorizationInfo.addStringPermissions(permsSet);
}
return simpleAuthorizationInfo;
} else if (principals.getPrimaryPrincipal() instanceof Admin) {
// 整个用户对象当主用户信息
Admin admin = (Admin) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
List<Role> roleList = roleService.getRoleByUserId(admin.getId());
List<Perms> permsList = permsService.getPermsByUserId(admin.getId());
if (!CollectionUtils.isEmpty(roleList)) {
Set<String> roleSet = roleList.stream().map(Role::getName).collect(Collectors.toSet());
simpleAuthorizationInfo.setRoles(roleSet);
}
if (!CollectionUtils.isEmpty(permsList)) {
Set<String> permsSet = permsList.stream().map(Perms::getName).collect(Collectors.toSet());
simpleAuthorizationInfo.addStringPermissions(permsSet);
}
return simpleAuthorizationInfo;
}
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.info("================ 认证 ===============");
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
// 在工厂中获取service对象
AdminService adminService = (AdminService) BeanUtil.getBean("adminService");
List<Admin> adminList = adminService.getAdminByUsername(upToken.getUsername());
Assert.state(adminList.size() < 2, "同一个用户名存在两个账户");
if (adminList.size() == 0) {
throw new UnknownAccountException("找不到用户(" + upToken.getUsername() + ")的帐号信息");
}
Admin admin = adminList.get(0);
// 1、主身份信息,在授权存入缓存时候,会以第一个参数当key,用对象当key也可以
//return new SimpleAuthenticationInfo(admin.getUsername(), admin.getPassword(), new MySimpleByteSource(admin.getSalt()), this.getName());
return new SimpleAuthenticationInfo(admin, admin.getPassword(), new MyByteSource(admin.getSalt()), this.getName());
}
}
3.4 添加注解
在Controller里的方法使用注解即可进行认证授权
// @RequiresPermissions("hello:helloadmin:*")
@RequiresRoles("admin")
@GetMapping("/helloAdmin")
public String helloAdmin(){
return "hello admin";
}
// @RequiresPermissions("hello:hellouser:*")
@RequiresRoles("user")
@GetMapping("/helloUser")
public String helloUser(){
return "hello user";
}
@RequiresPermissions("hello:helloadmin:*")
@GetMapping("/helloAdminPerm")
public String helloAdminPerm(){
return "hello adminPerm";
}
@RequiresPermissions("user:hello:*")
@GetMapping("/helloUserPerm")
public String helloUserPerm(){
return "hello userPerm";
}
3.5 登录
返回sessionId当做token
@PostMapping("login")
public ResponseResult login(@RequestBody JSONObject obj) {
String username = obj.getString("username");
String password = obj.getString("password");
Subject subject = SecurityUtils.getSubject();
try {
subject.login(new UsernamePasswordToken(username, password));
} catch (UnknownAccountException e) {
e.printStackTrace();
log.info("用户名错误");
return ResponseResult.fail("用户名错误");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
log.info("密码错误");
return ResponseResult.fail("密码错误");
}
return ResponseResult.ok(subject.getSession().getId());
}
3.6 注册
@Override
public ResponseResult register(Admin admin) {
String salt = SaltUtils.getSalt(8);
admin.setSalt(salt);
// 加盐,并散列1024次,与ShiroConfig中配置保持一致
Md5Hash md5Hash = new Md5Hash(admin.getPassword(), salt, 1024);
admin.setPassword(md5Hash.toHex());
if (getCountByUsername(admin.getUsername()) > 0) {
return ResponseResult.fail("用户名已存在");
}
if (save(admin)) {
return ResponseResult.ok();
} else {
return ResponseResult.fail("注册失败");
}
}
4.认证和授权使用redis缓存
4.1 ShiroConfig
添加开启缓存
@Bean
public Realm realm() {
CustomerRealm customerRealm = new CustomerRealm();
// 修改凭证校验匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 设置加密算法为MD5
credentialsMatcher.setHashAlgorithmName("MD5");
// 设置散列次数,必须跟注册一样
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
// 开启缓存管理
// customerRealm.setCacheManager(new EhCacheManager());
customerRealm.setCacheManager(new RedisCacheManager());
customerRealm.setCachingEnabled(true);
customerRealm.setAuthorizationCachingEnabled(true); // 授权
customerRealm.setAuthorizationCacheName("authorizationCache");
customerRealm.setAuthenticationCachingEnabled(true); // 认证
customerRealm.setAuthenticationCacheName("authenticationCache");
return customerRealm;
}
4.2 自定义RedisCacheManager
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
/**
* @version 1.0
* @author: hjianfeng
* @date: 2022/4/2 19:46
*/
public class RedisCacheManager implements CacheManager {
/**
*
* @param cacheName 认证或者是授权缓存的名字
* @param <K>
* @param <V>
* @return
* @throws CacheException
*/
@Override
public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
System.out.println("cacheName:"+cacheName);
return new RedisShiroCache<>(cacheName);
}
}
4.3 自定义RedisShiroCache
若主体应用使用了FastJson2JsonRedisSerializer或Jackson2JsonRedisSerializer进行序列化,如果直接使用,会导致SimpleAuthenticationInfo反序列化失败,则这边需要自定义一个redisTemplate进行操作。
import com.example.shirodemo.utils.BeanUtil;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.Collection;
import java.util.Set;
/**
* @version 1.0
* @author: hjianfeng
* @date: 2022/4/2 19:52
*/
public class RedisShiroCache<K, V> implements Cache<K, V> {
private String cacheName;
public RedisShiroCache(String cacheName) {
this.cacheName = cacheName;
}
@Override
public V get(K k) throws CacheException {
System.out.println("get key:" + cacheName + ":" + k);
return (V) getRedisTemplate().opsForHash().get(cacheName, k.toString());
}
@Override
public V put(K k, V v) throws CacheException {
System.out.println("put key:" + cacheName + ":" + k);
System.out.println("put value:" + v);
getRedisTemplate().opsForHash().put(cacheName, k.toString(), v);
return v;
}
@Override
public V remove(K k) throws CacheException {
getRedisTemplate().opsForHash().delete(cacheName, k.toString());
return null;
}
@Override
public void clear() throws CacheException {
getRedisTemplate().delete(cacheName);
}
@Override
public int size() {
return getRedisTemplate().opsForHash().size(cacheName).intValue();
}
@Override
public Set<K> keys() {
return getRedisTemplate().opsForHash().keys(cacheName);
}
@Override
public Collection<V> values() {
return getRedisTemplate().opsForHash().values(cacheName);
}
private RedisTemplate getRedisTemplate() {
RedisTemplate redisTemplate = (RedisTemplate) BeanUtil.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
4.4 自定义盐
复制SimpleByteSource添加无参构造函数,并把bytes的final去掉
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.codec.Hex;
import org.apache.shiro.util.ByteSource;
import java.io.File;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Arrays;
/**
* @version 1.0
* @author: hjianfeng
* @date: 2022/4/2 20:41
*
* 自定义salt实现序列化, 与SimpleByteSource的区别,只是为了序列化,
* 反序列化需要一个无参的构造,所以又加上无参构造
*/
public class MySimpleByteSource implements ByteSource,Serializable {
private byte[] bytes;
private String cachedHex;
private String cachedBase64;
public MySimpleByteSource() {
}
public MySimpleByteSource(byte[] bytes) {
this.bytes = bytes;
}
public MySimpleByteSource(char[] chars) {
this.bytes = CodecSupport.toBytes(chars);
}
public MySimpleByteSource(String string) {
this.bytes = CodecSupport.toBytes(string);
}
public MySimpleByteSource(ByteSource source) {
this.bytes = source.getBytes();
}
public MySimpleByteSource(File file) {
this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(file);
}
public MySimpleByteSource(InputStream stream) {
this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(stream);
}
public static boolean isCompatible(Object o) {
return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
}
public byte[] getBytes() {
return this.bytes;
}
public boolean isEmpty() {
return this.bytes == null || this.bytes.length == 0;
}
public String toHex() {
if (this.cachedHex == null) {
this.cachedHex = Hex.encodeToString(this.getBytes());
}
return this.cachedHex;
}
public String toBase64() {
if (this.cachedBase64 == null) {
this.cachedBase64 = Base64.encodeToString(this.getBytes());
}
return this.cachedBase64;
}
public String toString() {
return this.toBase64();
}
public int hashCode() {
return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
}
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o instanceof ByteSource) {
ByteSource bs = (ByteSource)o;
return Arrays.equals(this.getBytes(), bs.getBytes());
} else {
return false;
}
}
private static final class BytesHelper extends CodecSupport {
private BytesHelper() {
}
public byte[] getBytes(File file) {
return this.toBytes(file);
}
public byte[] getBytes(InputStream stream) {
return this.toBytes(stream);
}
}
}
5. session使用redis缓存
5.1 创建RedisSessionDAO
import com.example.shirodemo.utils.BeanUtil;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
/**
* @version 1.0
* @author: hjianfeng
* @date: 2022/4/3 14:29
*/
public class RedisSessionDAO extends EnterpriseCacheSessionDAO {
private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class);
// session 在redis过期时间是30分钟30*60
public static int SESSION_EXPIRE_TIME = 30 * 60 * 1000;
private static String prefix = "shiro:redis:session:";
private RedisTemplate redisTemplate;
// 创建session,保存到数据库
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = super.doCreate(session);
logger.debug("创建session:{}", session.getId());
getRedisTemplate().opsForValue().set(prefix + sessionId.toString(), session);
return sessionId;
}
// 获取session
@Override
protected Session doReadSession(Serializable sessionId) {
logger.debug("获取session:{}", sessionId);
// 先从缓存中获取session,如果没有再去数据库中获取
Session session = super.doReadSession(sessionId);
if (session == null) {
session = (Session) getRedisTemplate().opsForValue().get(prefix + sessionId.toString());
}
return session;
}
// 更新session的最后一次访问时间
@Override
protected void doUpdate(Session session) {
super.doUpdate(session);
logger.debug("获取session:{}", session.getId());
String key = prefix + session.getId().toString();
if (!getRedisTemplate().hasKey(key)) {
getRedisTemplate().opsForValue().set(key, session);
}
getRedisTemplate().expire(key, SESSION_EXPIRE_TIME, TimeUnit.MILLISECONDS);
}
// 删除session
@Override
protected void doDelete(Session session) {
logger.debug("删除session:{}", session.getId());
super.doDelete(session);
getRedisTemplate().delete(prefix + session.getId().toString());
}
private RedisTemplate getRedisTemplate() {
if (redisTemplate == null) {
redisTemplate = new RedisTemplate();
RedisTemplate redisTemp = (RedisTemplate) BeanUtil.getBean("redisTemplate");
redisTemplate.setConnectionFactory(redisTemp.getConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
}
return redisTemplate;
}
}
5.2 创建SessionManager
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
/**
* 原来使用session
*/
public class AdminWebSessionManager extends DefaultWebSessionManager {
public static final String LOGIN_TOKEN_KEY = "X-Shilaogaizao-Admin-Token";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(LOGIN_TOKEN_KEY);
if (!StringUtils.isEmpty(id)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
return super.getSessionId(request, response);
}
}
}
5.3 配置ShiroConfig
5.3.1 SecurityManager配置session
//2. 创建安全管理器
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 给安全管理器设置Realm
securityManager.setRealm(realm);
// 设置session
securityManager.setSessionManager(sessionManager());
return securityManager;
}
5.3.2 配置session相关
/**
* 配置Session管理器
* @Author Sans
* @CreateTime 2019/6/12 14:25
*/
@Bean
public SessionManager sessionManager() {
AdminWebSessionManager shiroSessionManager = new AdminWebSessionManager();
shiroSessionManager.setSessionDAO(new RedisSessionDAO());
shiroSessionManager.setGlobalSessionTimeout(RedisSessionDAO.SESSION_EXPIRE_TIME);
return shiroSessionManager;
}
*setGlobalSessionTimeout设定为毫秒
6. 使用jwt
6.1 思路
之前以为只能定义一个Realm,所以一直绕不出来,
以为:(错误思路)
1:账号密码登录,生成Token;
2:用自定义拦截器,拦截Token,使用某种手段获取到用户信息,绑定给Shiro
正确思路:
1:定义一个Realm专门处理UsernamePasswordToken类型登录,只要验证过,就用JwtUtil生成一个Token返回给前端
2:定义另一个Realm专门处理BearerToken(持票人,shiro自带),专门用来处理持有Token访问的请求
3:定义一个全局的Filter JwtFilter用于拦截需要认证的请求,并封装成BearerToken调用login重走认证路,只不过当前只会走2定义的Realm
4:禁用Session,并配置JwtFilter拦截所有需要认证的请求
说明:
1:两个Realm只有认证部分有所不同,授权部分一模一样,使用思路是:
1.1:账号密码登录,走UsernamePasswordToken,认证成功,生成jwtToken返回前端
1.2:用户每次请求,在head持有jwtToken,请求时被JwtFilter拦截,调用login,走TokenValidateRealm
2:由于CustomerRealm只用来处理账号密码登录,所以它里面的授权代码,压根就不会执行到,而是只会执行TokenValidateRealm里的授权
6.2 代码实现
6.2.1 修改CustomerRealm
// 添加如下代码,其实主要是配置supports,配置上这个,主要是告诉shiro,这个Realm只处理这个类型的ShiroToken
/**
* 标识这个Realm只处理这种Token
* 首先是覆盖getAuthenticationTokenClass方法,此时设定返回值为UsernamePasswordToken.class。
* shiro的机制是根据login方法中传入的token类型来分配realm,
* 步骤1中是UsernamePasswordToken,所以分配给本realm来处理。
*
* @return
*/
@Override
public Class getAuthenticationTokenClass() {
return UsernamePasswordToken.class;
}
/**
* 限定这个realm只能处理JwtToken(不加的话会报错)
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
6.2.2 新增TokenValidateRealm
import com.example.shirodemo.pojo.Admin;
import com.example.shirodemo.pojo.Perms;
import com.example.shirodemo.pojo.Role;
import com.example.shirodemo.service.AdminService;
import com.example.shirodemo.service.PermsService;
import com.example.shirodemo.service.RoleService;
import com.example.shirodemo.utils.BeanUtil;
import com.example.shirodemo.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
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.util.Assert;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @version 1.0
* @author: hjianfeng
* @date: 2022/4/4 10:34
*/
@Slf4j
public class TokenValidateRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("================ 授权 ===============");
// 获取身份信息
if (principals == null) {
throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
}
RoleService roleService = (RoleService) BeanUtil.getBean("roleService");
PermsService permsService = (PermsService) BeanUtil.getBean("permsService");
if (principals.getPrimaryPrincipal() instanceof String) {
// 用username当SimpleAuthenticationInfo主身份信息
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
List<Role> roleList = roleService.getRoleByUsername(username);
List<Perms> permsList = permsService.getPermsByUsername(username);
if (!CollectionUtils.isEmpty(roleList)) {
Set<String> roleSet = roleList.stream().map(Role::getName).collect(Collectors.toSet());
simpleAuthorizationInfo.setRoles(roleSet);
}
if (!CollectionUtils.isEmpty(permsList)) {
Set<String> permsSet = permsList.stream().map(Perms::getName).collect(Collectors.toSet());
simpleAuthorizationInfo.addStringPermissions(permsSet);
}
return simpleAuthorizationInfo;
} else if (principals.getPrimaryPrincipal() instanceof Admin) {
// 整个用户对象当主用户信息
Admin admin = (Admin) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
List<Role> roleList = roleService.getRoleByUserId(admin.getId());
List<Perms> permsList = permsService.getPermsByUserId(admin.getId());
if (!CollectionUtils.isEmpty(roleList)) {
Set<String> roleSet = roleList.stream().map(Role::getName).collect(Collectors.toSet());
simpleAuthorizationInfo.setRoles(roleSet);
}
if (!CollectionUtils.isEmpty(permsList)) {
Set<String> permsSet = permsList.stream().map(Perms::getName).collect(Collectors.toSet());
simpleAuthorizationInfo.addStringPermissions(permsSet);
}
return simpleAuthorizationInfo;
}
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
BearerToken bearerToken = (BearerToken) token;
String bearerTokenString = bearerToken.getToken();
String username = JwtUtil.get(bearerTokenString, "username", String.class);
if (StringUtils.isBlank(username)) {
throw new UnknownAccountException("找不到用户(" + username + ")的帐号信息");
}
AdminService adminService = (AdminService) BeanUtil.getBean("adminService");
List<Admin> adminList = adminService.getAdminByUsername(username);
Assert.state(adminList.size() < 2, "同一个用户名存在两个账户");
if (adminList.size() == 0) {
throw new UnknownAccountException("找不到用户(" + username + ")的帐号信息");
}
Admin admin = adminList.get(0);
// 1、主身份信息,在授权存入缓存时候,会以第一个参数当key,用对象当key也可以,但最好用username,所以此处修改一下
// return new SimpleAuthenticationInfo(admin.getUsername(), bearerTokenString, this.getName());
return new SimpleAuthenticationInfo(admin, bearerTokenString, this.getName());
}
@Override
public Class getAuthenticationTokenClass() {
//设置由本realm处理的token类型。BearerToken是在filter里自动装配的。
return BearerToken.class;
}
/**
* 限定这个realm只能处理JwtToken(不加的话会报错)
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof BearerToken;
}
}
6.2.3 新增JwtFilter
import com.example.shirodemo.shiro.exception.TokenException;
import com.example.shirodemo.utils.JwtUtil;
import com.example.shirodemo.utils.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.BearerToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @version 1.0
* @author: hjianfeng
* @date: 2022/4/3 21:02
*/
/**
* 鉴权登录拦截器
**/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
public static final String LOGIN_TOKEN_KEY = "X-Shilaogaizao-Admin-Token";
public static final String JWT_USER_ID = "userId";
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
return executeLogin(request, response);
} catch (Exception e) {
throw new AuthenticationException(e.getMessage());
}
}
/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(LOGIN_TOKEN_KEY);
if (StringUtils.isBlank(token)) {
throw new TokenException(ResponseResult.UNLOGIN, "token为空,请重新登录");
}
if (JwtUtil.isTokenExpired(token)) {
throw new TokenException(ResponseResult.UNLOGIN, "token已过期,请重新登录");
}
BearerToken jwtToken = new BearerToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 对跨域提供支持
*/
@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);
}
}
6.2.4 修改ShiroConfig
6.2.4.1 修改Realm
// 修改原始Realm以及新增tokenRealm
// 这个基本不变,之前在配置SecurityManager时是用注入形式,两个直接使用方法调用
//3. 创建自定义realm
@Bean("loginRealm")
public Realm realm() {
CustomerRealm customerRealm = new CustomerRealm();
// 修改凭证校验匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 设置加密算法为MD5
credentialsMatcher.setHashAlgorithmName("MD5");
// 设置散列次数,必须跟注册一样
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
// 开启缓存管理
// customerRealm.setCacheManager(new EhCacheManager());
customerRealm.setCacheManager(new RedisCacheManager());
customerRealm.setCachingEnabled(true);
customerRealm.setAuthorizationCachingEnabled(true); // 授权
customerRealm.setAuthorizationCacheName("authorizationCache");
customerRealm.setAuthenticationCachingEnabled(true); // 认证
customerRealm.setAuthenticationCacheName("authenticationCache");
return customerRealm;
}
// 创建专门用于处理Token的Realm
@Bean("tokenRealm")
public Realm tokenRealm() {
TokenValidateRealm customerRealm = new TokenValidateRealm();
// 开启缓存管理
// customerRealm.setCacheManager(new EhCacheManager());
customerRealm.setCacheManager(new RedisCacheManager());
customerRealm.setCachingEnabled(true);
customerRealm.setAuthorizationCachingEnabled(true); // 授权
customerRealm.setAuthorizationCacheName("authorizationCache");
customerRealm.setAuthenticationCachingEnabled(true); // 认证
customerRealm.setAuthenticationCacheName("authenticationCache");
return customerRealm;
}
6.2.4.2 修改SecurityManager
配置多个Realm,禁用Session
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 给安全管理器设置Realm
List<Realm> realms = new ArrayList<>();
realms.add(realm());
realms.add(tokenRealm());
securityManager.setRealms(realms);
// securityManager.setRealm(realm);
// 设置session
// securityManager.setSessionManager(sessionManager());
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-
* StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
6.2.4.3 修改shiroFilterFactoryBean
需要添加自定义过滤器,如下,并配置该过滤器为拦截所有需要认证的请求,猜测原本应该是有一个默认对应名称为**“authc”**的拦截器,名称需要一致
// 添加自定义过滤器
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
filterChainDefinitionMap.put("/**", "jwt"); // authc 请求这个资源需要认证和授权
原本代码:
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 给Filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
// 配置系统受限资源
// 配置系统公共资源
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 一定要放在通配符的上面
filterChainDefinitionMap.put("/auth/nologin", "anon"); // anon 不需要认证和授权, url可以匿名访问
filterChainDefinitionMap.put("/auth/login", "anon");
filterChainDefinitionMap.put("/auth/register", "anon");
filterChainDefinitionMap.put("/**", "authc"); // authc 请求这个资源需要认证和授权
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
// 默认认证界面路径
shiroFilterFactoryBean.setLoginUrl("/auth/nologin");
return shiroFilterFactoryBean;
}
修改后:
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 给Filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
// 添加自定义过滤器
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 配置系统受限资源
// 配置系统公共资源
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 一定要放在通配符的上面
filterChainDefinitionMap.put("/auth/nologin", "anon"); // anon 不需要认证和授权, url可以匿名访问
filterChainDefinitionMap.put("/auth/login", "anon");
filterChainDefinitionMap.put("/auth/register", "anon");
filterChainDefinitionMap.put("/**", "jwt"); // authc 请求这个资源需要认证和授权
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
// 默认认证界面路径
shiroFilterFactoryBean.setLoginUrl("/auth/nologin");
return shiroFilterFactoryBean;
}