记一次项目中使用到的shiro:
1、什么是shiro:
Shiro是Apache下的一个开源项目。shiro属于轻量级框架,相对于SpringSecurity简单的多,也没有SpringSecurity那么复杂。以下是我自己学习之后的记录。
官方架构图如下:
2.主要功能
shiro主要有三大功能模块:
- Subject:主体,一般指用户。
- SecurityManager:安全管理器,管理所有Subject,可以配合内部安全组件。(类似于SpringMVC中的DispatcherServlet)
- Realms:用于进行权限信息的验证,一般需要自己实现。
3.细分功能 - Authentication:身份认证/登录(账号密码验证)。
- Authorization:授权,即角色或者权限验证。
- Session Manager:会话管理,用户登录后的session相关管理。
- Cryptography:加密,密码加密等。
- Web Support:Web支持,集成Web环境。
- Caching:缓存,用户信息、角色、权限等缓存到如redis等缓存中。
- Concurrency:多线程并发验证,在一个线程中开启另一个线程,可以把权限自动传播过去。
- Testing:测试支持;
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。
- Remember Me:记住我,登录后,下次再来的话不用登录了。
(更多关于shiro是什么的文字请自行去搜索引擎找,本文主要记录springboot与shiro的集成)
首先先创建springboot项目,此处不过多描述。
3、上代码
1)引入jar包
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
2)实现我们自己的Realms:
新建类:UserRealm ,继承AuthorizingRealm,重写其认证和授权这两个主要方法:
package com.tzwy.mcsp.base.shiro;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.tzwy.mcsp.entity.sys.SysMenuEntity;
import com.tzwy.mcsp.entity.sys.UserAccount;
import com.tzwy.mcsp.mapper.sys.UserAccountDao;
import com.tzwy.mcsp.mapper.sys.UserDao;
import com.tzwy.mcsp.sys.service.CommonService;
import com.tzwy.mcsp.sys.service.SysMenuService;
import com.tzwy.mcsp.sys.service.UserAccountService;
import com.tzwy.mcsp.sys.service.UserService;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.stream.Collectors;
/**
* shiro用于认证用户~授权
**/
@Component
public class UserRealm extends AuthorizingRealm{
private static final Logger log= LoggerFactory.getLogger(UserRealm.class);
@Autowired
private UserDao userMapper;
@Autowired
private UserService userService;
@Autowired
private UserAccountDao userAccountMapper;
@Autowired
private UserAccountService userAccountService;
@Autowired
private CommonService commonService;
@Autowired
private SysMenuService sysMenuService;
/**
* 资源-权限分配 ~ 授权 ~ 需要将分配给当前用户的权限列表塞给shiro的权限字段中去
* @param principalCollection
* @return
* PrincipalCollection 是主体的集合
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取当前登录用户身份信息,此处得到的身份信息是账户表
UserAccount userAccount= (UserAccount) principalCollection.getPrimaryPrincipal();
Integer userId= userAccount.getUserId();
List<String> perms= new LinkedList<>();
//系统超级管理员拥有最高的权限,不需要发出sql的查询,直接拥有所有权限;否则,则需要根据当前用户id去查询权限列表
boolean isAdmin = commonService.isSuperadmin();//判断当前用户是否是超级管理员
if (isAdmin){
List<SysMenuEntity> list=sysMenuService.list();
if (list!=null && !list.isEmpty()){
perms=list.stream().map(SysMenuEntity::getPerms).collect(Collectors.toList());
}
}else{
perms=userMapper.queryAllPerms(userId);
}
//对于每一个授权编码进行 , 的解析拆分
Set<String> stringPermissions= new HashSet<>();
if (perms!=null && !perms.isEmpty()){
for (String p:perms){
if (StringUtils.isNotBlank(p)){
stringPermissions.addAll(Arrays.asList(StringUtils.split(p.trim(),",")));
}
}
}
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
info.setStringPermissions(stringPermissions);
return info;
}
/**
* 用户认证 ~ 登录认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token= (UsernamePasswordToken) authenticationToken;
final String userName=token.getUsername();
final String password=String.valueOf(token.getPassword());
log.info("用户名: {} 密码:{}",userName,password);
//SysUserEntity entity=sysUserDao.selectOne(new QueryWrapper<SysUserEntity>().eq("username",userName));
//根据username与password获取用户信息
UserAccount userAccount=userAccountMapper.selectOne(new QueryWrapper<UserAccount>().eq("account_code",
userName));
//账户不存在
if (userAccount==null){
throw new UnknownAccountException("账户不存在!");
}
//账户被禁用
if (0 == userAccount.getStatus()){
throw new DisabledAccountException("账户已被禁用,请联系管理员!");
}
//第一种 : 明文匹配
//账户名密码不匹配
/*if (!entity.getPassword().equals(password)){
throw new IncorrectCredentialsException("账户密码不匹配!");
}
SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(entity,password,getName());*/
//第三种验证逻辑
/*String realPassword=ShiroUtil.sha256(password,entity.getSalt());
if (StringUtils.isBlank(realPassword) || !realPassword.equals(entity.getPassword())){
throw new IncorrectCredentialsException("账户密码不匹配!");
}
SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(entity,password,getName());*/
//第二种验证逻辑-交给shiro的密钥匹配器去实现
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userAccount, userAccount.getPassword(), ByteSource.Util
.bytes(userAccount.getSalt()), getName());
return info;
}
/**
* 密码验证器~匹配逻辑 ~ 第二种验证逻辑
* @param credentialsMatcher
*/
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
shaCredentialsMatcher.setHashAlgorithmName(ShiroUtil.hashAlgorithmName);
shaCredentialsMatcher.setHashIterations(ShiroUtil.hashIterations);
super.setCredentialsMatcher(shaCredentialsMatcher);
}
}
解释: 可以看到下面的方法doGetAuthenticationInfo是认证,上面的方法doGetAuthorizationInfo是授权方法;
在认证方法里面有三种方式,可以根据自己喜好使用,当前采用shiro自带的校验方式;
3)把CustomRealm和SecurityManager等加入到spring容器:
ShiroConfig.java
package com.tzwy.mcsp.base.config;
import com.tzwy.mcsp.base.shiro.UserRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
//安全器管理-管理所有的subject
@Bean
public SecurityManager securityManager(UserRealm userRealm){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
securityManager.setRememberMeManager(null);
return securityManager;
}
//过滤链配置
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilter=new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//设定用户没有登录认证时的跳转链接、没有授权时的跳转链接
shiroFilter.setLoginUrl("/login.html");
shiroFilter.setUnauthorizedUrl("/");
//过滤器链配置
Map<String, String> filterMap = new LinkedHashMap();
filterMap.put("/swagger/**", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/webjars/**", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/statics/**", "anon");
filterMap.put("/login.html", "anon");
filterMap.put("/sys/login", "anon");
// filterMap.put("/sys/test/testMsg", "anon");
filterMap.put("/favicon.ico", "anon");
filterMap.put("/captcha.jpg", "anon");
filterMap.put("/**","authc");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
//关于Shiro的Bean生命周期的管理 下面两个方法配套使用
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
// @Bean(name = "sessionManager")
// public DefaultWebSessionManager sessionManager() {
// DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// // 设置session过期时间
// sessionManager.setGlobalSessionTimeout(0L);
// return sessionManager;
// }
}
3)自己又加了一个关于shiro的通用工具:
ShiroUtil.java
package com.tzwy.mcsp.base.shiro;
import com.tzwy.mcsp.entity.sys.User;
import com.tzwy.mcsp.exception.CommonException;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
//Shiro工具类
public class ShiroUtil {
//加密算法
public final static String hashAlgorithmName = "SHA-256";
//循环次数
public final static int hashIterations = 16;
public static String sha256(String password, String salt) {
return new SimpleHash(hashAlgorithmName, password, salt, hashIterations).toString();
}
//获取Shiro Session
public static Session getSession() {
return SecurityUtils.getSubject().getSession();
}
//获取Shiro Subject
public static Subject getSubject() {
return SecurityUtils.getSubject();
}
//获取Shiro中的真正主体
public static User getUserEntity() {
return (User)SecurityUtils.getSubject().getPrincipal();
}
public static Integer getUserId() {
return getUserEntity().getId();
}
public static void setSessionAttribute(Object key, Object value) {
getSession().setAttribute(key, value);
}
public static Object getSessionAttribute(Object key) {
return getSession().getAttribute(key);
}
public static boolean isLogin() {
return SecurityUtils.getSubject().getPrincipal() != null;
}
public static void logout() {
SecurityUtils.getSubject().logout();
}
/**
* 获取验证码
* @param key
* @return
*/
public static String getKaptcha(String key) {
Object object=getSessionAttribute(key);
if (object==null){
throw new CommonException("验证码已失效!");
}
String newCode=object.toString();
getSession().removeAttribute(key);
System.out.println("新的验证码:"+newCode);
return newCode;
}
public static void main(String[] args) {
String password="123456";
System.out.println(ShiroUtil.sha256(password, "YzcmCZNvbXocrsz9dm8e"));
}
}
4)添加freemark校验
因为项目中前端使用Freemarker,在配置时指定shiro校验:
引入jar包:
<!--freemarker-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
FreemarkerConfig.java
import com.tzwy.mcsp.base.shiro.ShiroVariable;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
//Freemarker配置
@Configuration
public class FreemarkerConfig {
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer(ShiroVariable shiroVariable){
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates");
Map<String, Object> variables = new HashMap<>(1);
variables.put("shiro", shiroVariable);
configurer.setFreemarkerVariables(variables);
Properties settings = new Properties();
settings.setProperty("default_encoding", "utf-8");
settings.setProperty("number_format", "0.##");
configurer.setFreemarkerSettings(settings);
return configurer;
}
}
ShiroVariable.java
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Component;
/**
* 推送给前端使用的shiro对象变量
**/
@Component
public class ShiroVariable {
/**
* 判断当前登录用户(主体)是否有 指定的权限
* @param permission 指定的权限
* @return
*/
public Boolean hasPermission(String permission){
Subject subject=SecurityUtils.getSubject();
/*if (subject!=null && subject.isPermitted(permission)){
return true;
}
return false;*/
return (subject!=null && subject.isPermitted(permission))? true : false;
}
}
这样在前端页面就可以具体控制哪些按钮可以显示可以不显示:
<#if shiro.hasPermission("sys:user:save")>
<a class="btn btn-primary" @click="add"><i class="fa fa-plus"></i> 新增</a>
</#if>
<#if shiro.hasPermission("sys:user:update")>
<a class="btn btn-primary" @click="update"><i class="fa fa-pencil-square-o"></i> 修改</a>
</#if>
<#if shiro.hasPermission("sys:user:delete")>
<a class="btn btn-primary" @click="del"><i class="fa fa-trash-o"></i> 删除</a>
</#if>
解释: “sys:user:save”是我们自定义的权限code,使用字符串;
完结;