之前使用 Shiro 鉴权的时候,一直用的是注解,如 @RequiresPermissions() 和 @RequiresRoles(),这种方法不利于维护和动态修改,代码侵入性强。所以,为了解决这个问题,通常都会采用URL鉴权,当写一个拦截器,获取请求的URL,然后查询当前登录用户的权限列表,判断请求的URL是否在权限列表的URL内,如果在则放行,否则拦截。
之前介绍了SpringSecurity权限管理,根据请求URL鉴权 ,本文就介绍一下 Shiro 的实现。
这里截图贴出几张表核心字段和部分数据
1. 用户表
2. 角色表
3. 权限表
4. 用户和角色关联表
5. 角色和权限关联表
springboot 版本 2.1.7.RELEASE
添加 shiro 依赖
1.自定义 Realm
package com.liuyanzhao.sens.config.shiro;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Validator;
import com.liuyanzhao.sens.entity.Permission;
import com.liuyanzhao.sens.entity.Role;
import com.liuyanzhao.sens.entity.User;
import com.liuyanzhao.sens.service.PermissionService;
import com.liuyanzhao.sens.service.RoleService;
import com.liuyanzhao.sens.service.UserService;
import com.liuyanzhao.sens.utils.LocaleMessageUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 默认的realm
*
* @author 言曌
* @date 2018/9/1 上午10:47
*/
@Slf4j
public class MyRealm extends AuthorizingRealm {
@Autowired
@Lazy
private UserService userService;
@Autowired
@Lazy
private RoleService roleService;
@Autowired
@Lazy
private PermissionService permissionService;
/**
* 认证信息(身份验证) Authentication 是用来验证用户身份
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
User user = userService.findByUserName(account);
if (user == null) {
return null;
}
//封装authenticationInfo,准备验证密码
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user, // 用户名
user.getUserPass(), // 密码
ByteSource.Util.bytes("sens"), // 盐
getName() // realm name
return authenticationInfo;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
User user = (User) principals.getPrimaryPrincipal();
List<Role> roles = roleService.listRolesByUserId(user.getId());
for (Role role : roles) {
authorizationInfo.addRole(role.getRole());
List<Permission> permissions = permissionService.listPermissionsByRoleId(role.getId());
for (Permission p : permissions) {
authorizationInfo.addStringPermission(p.getPermission());
}
}
return authorizationInfo;
}
}
注意:我们加密方式采用加盐(固定字符串 sens),md5十次
用户注册或添加用户或修改密码的时候
需要对用户密码加盐 sens,然后md5加密十次
可以使用 shiro 的 new Md5Hash(pwd, salt, i) 实现
如示例
user.setUserPass(new Md5Hash(password, "sens", 10).toString());
关于 permissionService 和 roleService 这里应该不用贴吧,大家应该能看懂吧
2. ShiroConfig
package com.liuyanzhao.sens.config.shiro;
import com.liuyanzhao.sens.config.properties.IgnoredUrlsProperties;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @author 言曌
* @date 2018/8/20 上午6:19
*/
@Configuration
public class ShiroConfig {
@Bean
IgnoredUrlsProperties getIgnoredUrlsProperties() {
return new IgnoredUrlsProperties();
}
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//自定义拦截器
Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
//访问权限配置
filtersMap.put("requestURL", getURLPathMatchingFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
//拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置不会被拦截的链接 顺序判断
List<String> urls = getIgnoredUrlsProperties().getUrls();
for (String url : urls) {
filterChainDefinitionMap.put(url, "anon");
}
filterChainDefinitionMap.put("/admin", "requestURL");
filterChainDefinitionMap.put("/admin/**", "requestURL");
filterChainDefinitionMap.put("/**", "anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
// 如果不设置默认会自动寻找Web工程根目录下的"/login"页面
shiroFilterFactoryBean.setLoginUrl("/admin/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm());
return securityManager;
}
/**
* 需要密码登录的realm
*
* @return MyShiroRealm
*/
@Bean
public MyRealm myRealm() {
MyRealm myRealm = new MyRealm();
myRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myRealm;
}
/**
* 凭证匹配器
* <p>
* 加密算法:md5加盐加密10次
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//散列的次数,md5("")
hashedCredentialsMatcher.setHashIterations(10);
return hashedCredentialsMatcher;
}
/**
* 访问 权限 拦截器
*
* @return
*/
public URLPathMatchingFilter getURLPathMatchingFilter() {
return new URLPathMatchingFilter();
}
}
之前我们都是用,authc 是 shiro 内部的,目前不满足我们的需求
filterChainDefinitionMap.put("/admin/**", "authc");
我们需要自己写一个根据 URL 过滤的拦截器,即 URLPathMatchingFilter 类
3. 自定义 URL 拦截器:URLPathMatchingFilter
package com.liuyanzhao.sens.config.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.PathMatchingFilter;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.util.Set;
/**
* URL拦截器
* @author 言曌
* @date 2019-10-12 17:56
*/
public class URLPathMatchingFilter extends PathMatchingFilter {
@Override
protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
//请求的url
String requestURL = getPathWithinApplication(request);
System.out.println("请求的url :" + requestURL);
Subject subject = SecurityUtils.getSubject();
if (!subject.isAuthenticated()) {
// 如果没有登录, 跳到登录页面
WebUtils.issueRedirect(request, response, "/admin/login");
return false;
}
//从session里读取当前用户的权限URL列表
Set<String> urls = (Set<String>) subject.getSession().getAttribute("permissionUrls");
if (urls.contains(requestURL)) {
return true;
}
//没有权限,跳到403页面
WebUtils.issueRedirect(request, response, "/403");
return false;
}
}
现在在 URL 拦截器里,从 session 里查询当前登录用户的权限URL列表,然后判断请求的URL是否在那个URL列表里就行。
(说明一下:登录成功的时候,我们会查询当前登录用户的权限列表,从里面获取URL列表,然后放到 Session 里。)
4. 将匿名访问的URL写到 application.yml 中
这里我们拦截 /admin/** 的页面
但是想要放行一些特殊的,如 /admin/login,/admin/register 这些是登录页面
之前我们都是直接写
filterChainDefinitionMap.put("/admin/login", "anno");
filterChainDefinitionMap.put("/admin/register", "anno");
但是一旦多起来比较麻烦,我们希望写在配置文件里
如下 application.yml
# 忽略鉴权url,即设置为anon的url
ignored:
urls:
- /admin/login
- /admin/getLogin
- /admin/register
- /admin/getRegister
- /admin/forget
- /admin/getForget
然后创建一个类 IgnoredUrlsProperties
package com.liuyanzhao.sens.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
/**
* @author liuyanzhao
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "ignored")
public class IgnoredUrlsProperties {
private List<String> urls = new ArrayList<>();
}
通过注入这个类就能获取 urls
但是在 ShiroConfig 里无法直接注入,如
/**
* 无法注入,ignoredUrlsProperties为null
* 需要改成 @Bean
*/
@Autowired
private IgnoredUrlsProperties ignoredUrlsProperties;
这样是不行的
需要改成 @Bean 这种,手动 new 一个
@Bean
IgnoredUrlsProperties getIgnoredUrlsProperties() {
return new IgnoredUrlsProperties();
}
至此 shiro 的相关配置就结束了
下面介绍一下登录和登出
1.登录
/**
* 验证登录信息
*
* @param account 用户名
* @param password password 密码
* @return JsonResult JsonResult
*/
@PostMapping(value = "/getLogin")
@ResponseBody
public JsonResult getLogin(String account, String password) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(account, password);
try {
subject.login(token);
if (subject.isAuthenticated()) {
User user = (User) subject.getPrincipal();
// 将用户的权限URL列表放到 session 中
Set<String> permissionUrls = permissionService.findPermissionUrlsByUserId(user.getId());
subject.getSession().setAttribute("permissionUrls", permissionUrls);
return new JsonResult(200, "登录成功");
}
} catch (UnknownAccountException e) {
log.info("UnknownAccountException -- > 账号不存在:");
return new JsonResult(500, "账号不存在");
} catch (IncorrectCredentialsException e) {
return new JsonResult(500, "密码错误");
}
} catch (LockedAccountException e) {
log.info("LockedAccountException -- > 账号被锁定");
return new JsonResult(500, "账号被锁定");
} catch (Exception e) {
log.info(e.getMessage());
}
return new JsonResult(500, "服务器内部错误");
}
2.登出
/**
* 退出登录
*
* @return 重定向到/admin/login
*/
@GetMapping(value = "/logOut")
public String logOut() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/admin/login";
}