欢迎光临我的博客[http://poetize.cn],前端使用Vue2,聊天室使用Vue3,后台使用Spring Boot
Shiro 核心 API
Subject:用户主体(每次请求都会创建Subject)。
principal:代表身份。可以是用户名、邮件、手机号码等等,用来标识一个登录主体的身份。
credential:代表凭证。常见的有密码,数字证书等。
SecurityManager:安全管理器(关联 Realm),用于安全校验。
Realm:Shiro 连接数据的桥梁。
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率。
CacheManager:缓存控制器,来管理如用户、角色、权限等缓存的控制器。
SessionManager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中。
SessionDAO:会话储存。
Shiro 认证与授权
身份认证:
Step1:应用程序代码调用 Subject.login(token) 方法后,传入代表最终用户身份的 AuthenticationToken 实例 Token。
Step2:将 Subject 实例委托给应用程序的 SecurityManager(Shiro 的安全管理)并开始实际的认证工作。
Step3、4、5:SecurityManager 根据具体的 Realm 进行安全认证。
权限认证(涉及到三张表:用户表、角色表和权限表):
权限(Permission):即操作资源的权利(添加、修改、删除、查看操作的权利)。
角色(Role):指的是用户担任的角色,一个角色可以有多个权限。
用户(User):在 Shiro 中,代表访问系统的用户,即上面提到的 Subject 认证主体。
请求步骤:
浏览器发出第一次请求的时候,去redis里找不到对应的session,会进入到登录页。
当在登录页输入完正确的账号密码后,才能登录成功。
根据登录成功后的session生成sessionId,并传到前端浏览器中,浏览器以cookie存储,同时将session存储到redis中。
每次浏览器访问后台,都会刷新session的过期时间expireTime。
当浏览器再次请求时,将当前浏览器中的所有的cookie设置到request headers请求头中。
根据传入的sessionId串到共享的redis存储中匹配。
如果匹配不到,则会跳转到登录页,如果匹配成功,则会访问通过。
Spring Boot Shiro 依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</dependency>
自定义 Realm
自定义 Realm 需要继承 AuthorizingRealm 类,该类封装了很多方法,且继承自 Realm 类。
重写以下两个方法:
doGetAuthenticationInfo() 方法:用来验证当前登录的用户,获取认证信息。
doGetAuthorizationInfo() 方法:为当前登录成功的用户授予权限和分配角色。
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 登录成功的用户授予权限和分配角色
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取用户名
String account = (String) principals.getPrimaryPrincipal();
//从数据库查询用户角色信息
User user = userService.getUserByAccount(account);
//设置角色
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
Set<String> roles = new HashSet();
if (user.getAdmin()) {
roles.add(Base.ROLE_ADMIN);
}
authorizationInfo.setRoles(roles);
return authorizationInfo;
}
/**
* 执行认证逻辑
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户名
String account = (String) token.getPrincipal();
//从数据库查询该用户
User user = userService.getUserByAccount(account);
if (null == user) {
throw new UnknownAccountException(); //没找到该帐号
}
if (UserStatus.blocked.equals(user.getStatus())) {
throw new LockedAccountException(); //帐号锁定
}
//传入用户名和密码进行身份认证,并返回认证信息
return new SimpleAuthenticationInfo(
user.getAccount(),
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()), //盐(密码加盐加密处理)
getName()
);
}
}
自定义 SessionDAO
Cachemanager缓存里可以包含权限认证的缓存、用户及权限信息的缓存等,也可以做Session缓存。
SessionDAO是做Session持久化的,可以使用Redis来存储。
默认 SessionDAO:MemorySessionDAO:
将Session保存在内存中,存储结构是ConcurrentHashMap。
public class MemorySessionDAO extends AbstractSessionDAO {
private ConcurrentMap<Serializable, Session> sessions = new ConcurrentHashMap();
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
this.storeSession(sessionId, session);
return sessionId;
}
}
/**
* 将Session保存到Redis
*/
public class CustomSessionDAO extends CachingSessionDAO {
@Autowired
private RedisTemplate redisTemplate;
//默认缓存过期时间:30分钟
public final static long DEFAULT_EXPIRE = 60 * 30;
@Override
protected Serializable doCreate(Session session) {
//创造SessionId
Serializable sessionId = generateSessionId(session);
//注册SessionId
assignSessionId(session, sessionId);
//缓存Session
redisTemplate.opsForValue().set(sessionId.toString(), session, DEFAULT_EXPIRE, TimeUnit.SECONDS);
return sessionId;
}
@Override
protected void doUpdate(Session session) {
if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
//会话过期/停止
return;
}
redisTemplate.opsForValue().set(session.getId().toString(), session, DEFAULT_EXPIRE, TimeUnit.SECONDS);
}
@Override
protected void doDelete(Session session) {
redisTemplate.delete(session.getId().toString());
}
@Override
protected Session doReadSession(Serializable sessionId) {
return (Session) redisTemplate.opsForValue().get(sessionId.toString());
}
}
自定义 SessionManager(待完善)
public class CustomSessionManager extends DefaultWebSessionManager {
public static final String TOKEN = "token";
/**
* 调用登陆接口的时候,是没有token的。
* 登陆成功后,产生了token,我们把它放到request中。
* 返回结果给客户端的时候,把它从request中取出来,并且传递给客户端。
* 客户端每次带着这个token过来,就相当于是浏览器的cookie的作用,也就能维护会话了。
*/
@Override
public Serializable getSessionId(SessionKey key) {
Serializable sessionId = key.getSessionId();
if(sessionId == null && WebUtils.isWeb(key)){
HttpServletRequest request = WebUtils.getHttpRequest(key);
HttpServletResponse response = WebUtils.getHttpResponse(key);
sessionId = this.getSessionId(request,response);
}
HttpServletRequest request = WebUtils.getHttpRequest(key);
request.setAttribute(TOKEN,sessionId.toString());
return sessionId;
}
/**
* DefaultWebSessionManager默认实现中,是通过Cookie确定SessionId。
* 重写时,只需要把获取SessionId的方式变更为在request header中获取即可。
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String id = httpRequest.getHeader(TOKEN);
if (!StringUtils.isEmpty(id)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
}
return super.getSessionId(request, response);
}
}
配置类 ShiroConfig
@Configuration
public class ShiroConfig {
/**
* 创建 ShiroFilterFactoryBean
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//LinkedHashMap 是有序的,进行顺序拦截器配置
Map<String, String> filterMap = new LinkedHashMap();
filterMap.put("/static/**", "anon"); //无需认证可以访问
filterMap.put("/login", "anon");
filterMap.put("/register", "anon");
//配置退出过滤器,其中具体的退出代码Shiro已经替我们实现了,登出后跳转配置的LoginUrl
filterMap.put("/logout", "logout");
filterMap.put("/**/create", "authc"); //必须认证才可以访问
filterMap.put("/**/update", "authc");
filterMap.put("/**/delete", "authc");
filterMap.put("/upload", "authc");
filterMap.put("/admin", "perms[admin]"); //资源必须得到资源权限才能访问,多个参数写法:perms["admin,user"]
filterMap.put("/admin", "role[admin]"); //资源必须得到角色权限才能访问
filterMap.put("/**", "anon");
//设置默认登录的URL,身份认证失败会访问该URL
shiroFilterFactoryBean.setLoginUrl("/login");
//身份认证设置成功之后要跳转的URL
shiroFilterFactoryBean.setSuccessUrl("/index");
//设置未授权界面,权限认证失败会访问该URL
shiroFilterFactoryBean.setUnauthorizedUrl("/unAuthorized");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
/**
* 配置安全管理器:DefaultWebSecurityManager
*/
@Bean
public SecurityManager securityManager(CustomRealm realm, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
/**
* 创建自定义 Realm
*/
@Bean
public CustomRealm customRealm() {
CustomRealm shiroRealm = new CustomRealm();
//配置密码加密
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("md5"); //加密方式
matcher.setHashIterations(2); //加密次数
shiroRealm.setCredentialsMatcher(matcher);
return shiroRealm;
}
/**
* 配置 SessionManager
*/
@Bean
public SessionManager sessionManager() {
CustomSessionManager customSessionManager = new CustomSessionManager();
//session过期时间:1小时(默认半小时)
customSessionManager.setGlobalSessionTimeout(60 * 60 * 1000);
customSessionManager.setSessionDAO(new CustomSessionDAO());
return customSessionManager;
}
/**
* Spring Boot Shiro 开启注释
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
Session的查询、刷新
SimpleSession几个属性:
Serializable id:session id;
Date startTimestamp:session的创建时间;
Date stopTimestamp:session的失效时间;
Date lastAccessTime:session的最近一次访问时间,初始值是startTimestamp
long timeout:session的有效时长,默认30分钟
boolean expired:session是否到期
Map<Object, Object> attributes:session的属性容器
查询:Session session = SecurityUtils.getSubject().getSession(); //返回的就是绑定在当前subjuct的session。
刷新:SimpleSession的touch()
public void touch() {
this.lastAccessTime = new Date();
}
Web应用,每次进入ShiroFilter都会自动调用session.touch()来更新最后访问时间。
过期时间判断:当前时间-lastAccessTime=是否超过有效时长。
Shiro支持三种方式的授权
1、编程式,通过写if/else授权代码块
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("admin")) {
// 有权限,执行相关业务
} else {
// 无权限,给相关提示
}
2、注解式,通过在执行的Java方法上放置相应的注解完成
@RequiresPermissions("admin")
public List<User> listUser() {
// 有权限,获取数据
}
3、JSP/GSP标签,在JSP/GSP页面通过相应的标签完成
<shiro:hasRole name="admin">
<!-- 有权限 -->
</shiro:hasRole>
Spring Boot 集成 Shiro 和 Ehcache(待完善)
引入配置文件 ehcache.xml:
application.xml配置文件添加:spring.cache.ehcache.config=classpath:ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="java.io.tmpdir/shiro-cache"/>
<defaultCache //默认缓存策略
eternal="false" //对象是否永久有效,一但设置了,timeout将不起作用
maxElementsInMemory="1000" //缓存最大元素数目
overflowToDisk="false" //当内存中对象数量达到maxElementsInMemory时,Ehcache将对象写到磁盘中。
diskPersistent="false" //是否在磁盘上持久化。指重启JVM后,数据是否有效。默认为false
timeToIdleSeconds="0" //设置对象在失效前的允许闲置时间(单位:s),默认是0(永久有效)。
timeToLiveSeconds="600" //设置对象在失效前允许存活时间(单位:s),默认是0(永久有效)。
//当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存
//LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)
memoryStoreEvictionPolicy="LRU" />
<cache
name="users" //缓存名称
eternal="false"
maxElementsInMemory="500"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="300"
memoryStoreEvictionPolicy="LRU" />
</ehcache>
Controller 与 Service 使用
Controller:
@PostMapping("/login")
public Result login(@RequestBody User user) {
Result r = new Result();
//获取Subject用户主体
Subject subject = SecurityUtils.getSubject();
//封装用户数据
UsernamePasswordToken token = new UsernamePasswordToken(user.getAccount(), user.getPassword());
try {
//执行认证操作(调用UserRealm中的方法认证)
subject.login(token);
//认证通过
User currentUser = userService.getUserByAccount(user.getAccount());
subject.getSession().setAttribute(Base.CURRENT_USER, currentUser);
r.setResultCode(ResultCode.SUCCESS);
r.getData().put("token", subject.getSession().getId());
} catch (UnknownAccountException e) {
r.setResultCode(ResultCode.USER_NOT_EXIST); //用户不存在
} catch (LockedAccountException e) {
r.setResultCode(ResultCode.USER_ACCOUNT_FORBIDDEN); //账号被锁定
}catch (IncorrectCredentialsException e) {
r.setResultCode(ResultCode.USER_LOGIN_PASSWORD_ERROR); //密码错误
} catch (AuthenticationException e) {
r.setResultCode(ResultCode.USER_LOGIN_ERROR); //认证错误(包含以上错误)
}
return r;
}
@PostMapping("/register")
public Result register(@RequestBody User user) {
Result r = new Result();
User temp = userService.getUserByAccount(user.getAccount());
if (null != temp) {
r.setResultCode(ResultCode.USER_HAS_EXISTED);
return r;
}
userService.saveUser(user);
r.setResultCode(ResultCode.SUCCESS);
return r;
}
Service:
@Override
@Transactional
public void saveUser(User user) {
//密码加密
String newPassword = new SimpleHash(
"md5",user.getPassword(),
ByteSource.Util.bytes("salt"),2).toHex();
user.setPassword(newPassword);
return userRepository.save(user);
}