最近两个月诸事不顺,有时候显得浮躁焦虑,也没处理好自己的情绪,小伙子,需要冷静。你还有3个月才二十四岁,你着什么急。只有你足够优秀,才能遇到更优秀的人。文字是让人静下来的好东西。2019年的第一篇博客就献给Shiro吧。
一、Shiro
Apache Shiro是一个Java安全框架,用来做身份验证(用户登录)、授权(权限控制)、密码和会话管理。常用的就是前两个模块。Shiro配置简单,使用起来无倾入性。比SpringSecurity更轻量级。
先对3个核心组件类有个印象,一定先理解这三个东西,对后面写代码和设计有帮助:
- Subject:可以把它看成当前请求访问系统的用户。会存储当前用户的信息,用户名密码等等。
- SecurityManager:管理所有用户的安全操作。它是Shiro框架的核心,Shiro通过SecurityManager来管理内部各个组件实例,并通过它来提供安全管理的各种服务。权限验证等等。
- Realm: 当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm(自己定义这个Realm)验证用户及其权限信息。
二、设计方案
本文只是介绍Shiro的基本使用和一般系统的登录,权限模块设计思路。
- system_user(id、username、password、role_id<system_role.id>):用户表
- system_role(id、role_name):角色表
- system_resources(id、resources_name、resources_url、permission_code):系统资源表,resources_url就是该资源的请求路径,譬如一个接口请求路径,permission_code是使用Shiro的权限控制注解是需要用到的code,后面会看到。此表有条数据【1,用户列表,/user/getList,permis[get]】后面会用到!!!!
- role_resources(id、role_id<system_role.id>、resources_id<system_resources.id>):角色资源关联表
大致表结构如上,很清晰易懂。
系统每个用户对应一个角色(本文就设计为一个用户只有一个角色,当然,你也可以设计多个角色的情况)。
传统的权限控制手段可能就是:用户登录–>查询数据库用户名和密码是否匹配–>匹配的话根据当前用户的role_id在角色资源关联表查询到该角色拥有的资源id集合–>关联系统资源表–>再去判断该用户是否有此资源的权限。Shiro可以包揽整个过程。
三、实现
一步一步看着来,不要着急。慢慢理解,我应该写的很详细了,不难懂。(这里只列核心代码)
pom.xml Shiro依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
本文只做两个事,把用户登录的验证和用户是否拥有某个接口的请求权限交给Shiro来处理。
1、先解决用户登录问题
try{
//省略了其他的常规代码,比如判断字段是否为空之类的
//此Subject就是开头提到的 代表当前用户
Subject subject = SecurityUtils.getSubject();
//用请求的用户名和密码创建UsernamePasswordToken(此类来自shiro包下)
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
//调用subject.login进行验证,验证不通过则会抛出AuthenticationException异常,然后自定义返回信息
subject.login(usernamePasswordToken);
//未抛异常 则验证通过
//此Session也来自shiro包 是对传统的HttpSession的封装,可以看做是一样的
Session session = subject.getSession();
//下面的是自定义的代码,随你怎么写
session.setAttribute(RequestConstans.USER_ID, checkUser.getId());
} catch (AuthenticationException e) {
//这行也是自定义的代码,随你怎么写
throw new BusinessException(BusinessErrorCode.USER_LOGIN_PWD_ERROR_FAIL);
}
这是截取的登录接口的一部分代码,这就是登录接口方法的处理了,很简单,通过subject.login(usernamePasswordToken);方法,就把登录验证交给Shiro来处理了。省略了原来自己去判断用户名跟密码是否匹配的过程。
扩展:hiro session和Spring session一样吗?
2、自定义的Realm,用来处理登录验证与鉴权
package com.web.config;
import com.mechat.backend.dao.CommercialMapper;
import com.mechat.backend.dao.SystemMenuMapper;
import com.mechat.backend.model.Commercial;
import com.mechat.backend.model.SystemMenu;
import com.mechat.backend.utils.UserConfig;
import com.mechat.common.auth.AESUtils;
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.beans.factory.annotation.Autowired;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author: WangRui
* @Date: 2019/1/22
* Time: 22:43
* Description:
*/
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private SystemMenuMapper systemMenuMapper;
@Autowired
private UserMapper userMapper;
/**
* 用户登录认证方法
* 上面不是调用了subject.login()方法嘛,就会进入到这个方法来进行具体登录验证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//authenticationToken.getPrincipal()是获得用户名
if (authenticationToken.getPrincipal() == null) {
throw new AuthenticationException("账号名为空,登录失败!");
}
//获取用户信息
String name = authenticationToken.getPrincipal().toString();
User user = new User();
user.setUserName(name);
//通过用户名在数据库查到该用户的信息
user = userMapper.selectByPKey(user);
if (user == null) {
//这里返回后会报出对应异常
throw new AuthenticationException("不存在的账号,登录失败!");
} else {
//这里验证authenticationToken和simpleAuthenticationInfo的信息
//getName() 是Shiro包下org.apache.shiro.realm.CachingRealm的方法,不是自定义的
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword, getName());
return simpleAuthenticationInfo;
}
}
/**
* 用户鉴权
* 用户请求有权限要求的接口时要经过此认证,
* 譬如我在某个Controller的方法上加了注解@RequiresPermissions(value = "permis[get]"),
* 那么该用户的角色拥有的资源必须要包含“permis[get]”权限才能访问此接口,
* “permis[get]”对应文章开头说的表结构那里的系统资源表中的permission_code
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取登录用户名,此用户名是在登录接口里new UsernamePasswordToken()时设置的
String account = (String) principalCollection.getPrimaryPrincipal();
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//根据用户账号查询拥有的所有资源权限 SystemMenu对应表system_resources,这是一条关联sql,
//通过用户账号-拿到角色id-在role_resources查询到拥有的resources_id-然后关联system_resources查询到拥有的所有资源
List<SystemMenu> systemMenuList = systemMenuMapper.findUserPermission(account);
//添加角色 因为此次只关联到权限permission,故暂不添加角色,只通过permission来鉴权
//添加角色对应的使用注解是 @RequiresRoles()
//simpleAuthorizationInfo.save("admin");
//添加权限 在这里就把该用户对应的角色拥有的所有的权限的permission_code添加到Shiro了,每次访问带有权限限制的接口时就会验证,拥有对应权限code的话就可以正常访问。
simpleAuthorizationInfo.addStringPermissions(systemMenuList.stream().map(systemMenu -> systemMenu.getPermission()).collect(Collectors.toList()));
return simpleAuthorizationInfo;
}
}
注解使用图解:
注意@RequiresPermissions(value = “permis[get]”) value的值就是对应资源的permission_code,见开头表结构设计那里!!!换句话说,此用户必须要有此资源权限才能访问这个Controller方法。
3、securityManager安全配置
自定义的MyShiroRealm写好了,要配置他才能使用。下面就是Shiro的配置类了:
定义了一个Properties,需要忽略验证的请求路径
@Component
@ConfigurationProperties
public class IgnoreAuthUrlProperties {
List<String> ignoreAuthUrl;
public List<String> getIgnoreAuthUrl() {
return ignoreAuthUrl;
}
public void setIgnoreAuthUrl(List<String> ignoreAuthUrl) {
this.ignoreAuthUrl = ignoreAuthUrl;
}
}
对应的配置:
package com.web.config;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.*;
/**
* @Author: WangRui
* @Date: 2019/1/22
* Time: 22:44
* Description:
*/
@Configuration
public class ShiroConfiguration {
@Autowired
private IgnoreAuthUrlProperties ignoreAuthUrlProperties;
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
return myShiroRealm;
}
@Bean
public CacheManager cacheManager() {
return new EhCacheManager();
}
@Bean
public SessionDAO sessionDAO() {
return new EnterpriseCacheSessionDAO();
}
@Bean
public SessionManager sessionManager(SessionDAO sessionDAO) {
DefaultWebSessionManager manager = new DefaultWebSessionManager();
manager.setSessionDAO(sessionDAO);
//设置session过期时间
manager.setGlobalSessionTimeout(3600000);
manager.setSessionValidationInterval(3600000);
return manager;
}
/**
* 权限管理,配置主要是Realm的管理认证
*/
@Bean
public SecurityManager securityManager(CacheManager cacheManager, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setSessionManager(sessionManager);
securityManager.setRealm(myShiroRealm());
securityManager.setCacheManager(cacheManager);
return securityManager;
}
/**
* Filter工厂,设置对应的过滤条件和跳转条件 这是重点!!!
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Set<String> urlSet = new HashSet<>(ignoreAuthUrlProperties.getIgnoreAuthUrl());
//必须采用LinkedHashMap有序存储过滤条件
Map<String, String> map = new LinkedHashMap<>();
//anon表示所有用户都可以不鉴权匿名访问
urlSet.stream().forEach(temp -> map.put(temp, "anon"));
//此路径必须放在最后 这是为啥一定使用LinkedHashMap的原因
map.put("/**", "authc");
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
//首页
shiroFilterFactoryBean.setSuccessUrl("/index");
shiroFilterFactoryBean.setUnauthorizedUrl("/login");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 加入shiro注解的使用,不加入这个注解不生效
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
至次,Shiro的登录验证和鉴权算是开发完成了。只是简单实用,Shiro还有其他很强大的功能。
有啥疑问网上搜一搜应该都能解决。也可以留言。