前言
shiro学习一:了解shiro,学习执行shiro的流程。使用springboot的测试模块学习shiro单应用(demo 6个)
代码所在地:https://github.com/fengfanli/springboot-shiro 记得给个星哦。
前面都是对shiro的初级学习和分析,本次博客就记录springboot整合shiro框架。
一、逻辑
1. 登录逻辑
shiro 做安全权限控制。那么shiro的过滤器和数据源的处理主要是针对token的认证和授权。
而用户的密码验证则还是在service层中进行处理。
- 首先将用户登录的接口
/user/login
在shiro过滤器中放开,不在拦截,设置为anon
- 发送post请求携带用户名和密码,进行登录。走这个请求时,在上一步已经被设置了白名单。走控制器,业务层service,在这里进行判断,用户名密码判断无误后,设置token和id,然后返回到前端。
- 总结一下:用户的密码登录和shiro是无关的,正常的判断即可。注意的是需要返回的数据,这里仅有id和token。token也是UUID生成的,并存在redis中。
- 访问其他接口时,拿着token放在请求头中,然后发请求,没有被shiro设置在白名单里的请求会被shiro拦截,拦截流程如下:
ShiroAccessControlFilter类:isAccessAllowed():onAccessDenied(),用到了ShiroUsernamePasswordToken类
->ShiroRealm类:doGetAuthenticationInfo方法
->ShiroHashedCredentialsMatcher类:doCredentialsMatch() - 根据博客三,这里重写的doCredentialsMatch()方法就是 shiro的过滤器的最后一步,也是至关重要的一步。
6. 接下来就可以去控制的接口了,如果遇到@RequiresPermissions() 注解,再去ShiroRealm类中doGetAuthorizationInfo()授权方法去授予权限即可,再去接口即可。
2. 项目目录结构
3. 开发逻辑
a. redis 开发工具类
redis的配置类和工具类,我就不再贴出,本博客主要写shiro的代码,代码可去GitHub上拉取下来观看
- config包下的RedisConfig配置类
- serializer包下的MyStringRedisSerializer序列化类
- utils包下的RedisUtil类
代码不再贴了,都在GitHub上
b. 密码加密工具类
- utils 包下的
PasswordUtils
工具类。 - PasswordEncoder密码编码类。
代码不再贴了,都在GitHub上
c. swagger配置
- config包下的 SwaggerConfig配置类
- 配置类上的启动注解
@EnableSwagger2
代码不再贴了,都在GitHub上
d. shiro开发 流程
- 先开发
ShiroConfig
,设置自定义过滤器ShiroAccessControlFilter
,并设置过滤器的白名单
对登录的请求设置为 anon。 - 开发
ShiroAccessControlFilter
拦截器,这里是对 token 的简单过滤验证,并进行主体提交,提交到自定义数据域 realm。
开发拦截器中,还要开发ShiroUsernamePasswordToken
,自己实现shiro认证机制,就要重写类UsernamePasswordToken
- 开发自定义realm:
ShiroRealm
类。数据域部分,进行授权和认证。
认证:对token进行过滤,用户名密码登录部分还是在业务层处理
授权:从数据库中获取,设计到表role、permission、user_role、role_permission
四个表 - 开发 token 的最后过滤处理
ShiroHashedCredentialsMatcher
,继承HashedCredentialsMatcher
类实现方法doCredentialsMatch()
以上四步其实就是 shiro 开发的全部,基本上都是配置式的代码。
二、shiro代码开发
1. shiroConfig.java
package com.feng.config;
import com.feng.shiro.ShiroAccessControlFilter;
import com.feng.shiro.ShiroHashedCredentialsMatcher;
import com.feng.shiro.ShiroRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
/**
* token 的过滤
* 自定义token 校验
* 学习说明:其实 下面的 ShiroHashedCredentialsMatcher(自定义的) 也继承了 HashedCredentialsMatcher。
* 需要在 CustomRealm bean 中进行设置
* @return
*/
@Bean(name = "shiroHashedCredentialsMatcher")
public ShiroHashedCredentialsMatcher shiroHashedCredentialsMatcher() {
return new ShiroHashedCredentialsMatcher();
}
/**
* 登录的 认证域
*
* @param hashedCredentialsMatcher
* @return
*/
@Bean(name = "shiroRealm")
public ShiroRealm getShiroRealm(@Qualifier("shiroHashedCredentialsMatcher") HashedCredentialsMatcher hashedCredentialsMatcher) {
ShiroRealm shiroRealm = new ShiroRealm();
// 自定义 处理 token 过滤
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher);
return shiroRealm;
}
/**
* shiro 的安全管理器
*
* @param shiroRealm
* @return
*/
@Bean(name = "securityManager")
public SecurityManager securityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm);
return securityManager;
}
/**
* shiro 的过滤器
* 需要了解 shiro 的权限关键字含义:
* anon,表示不拦截的路径
* authc,表示拦截的路径
*
* 匹配时,首先匹配 anon 的,然后最后匹配 authc
*
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
/*
* 自定义过滤器
* */
//自定义拦截器限制并发人数,参考博客:
LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
//用来校验token
filtersMap.put("token", new ShiroAccessControlFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
/*
* 以下为权限控制
* */
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/user/login", "anon");
// filterChainDefinitionMap.put("/user/test", "anon");
// 拦截所有
filterChainDefinitionMap.put("/**", "token,authc");
// 没有登录的用户请求需要登录的页面时自动跳转到登录页面。 配置 shiro 默认登录界面地址,
shiroFilterFactoryBean.setLoginUrl("/api/user/login");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 下面两个配置类 AuthorizationAttributeSourceAdvisor 和 DefaultAdvisorAutoProxyCreator,开启 shiro aop 注解 支持.
* 使用代理方式;所以需要开启代码支持;
* <p>
* 如果不加 使用 @RequirePermissions 无效
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
}
2. ShiroAccessControlFilter
package com.feng.shiro;
import com.alibaba.fastjson.JSON;
import com.feng.constant.Constant;
import com.feng.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName: CustomAccessControlerFilter
* @Description: 自定义的 token 过滤器。
* @createTime:
* @Author: 冯凡利
* @UpdateUser: 冯凡利
* @Version: 0.0.1
*/
/**
* 这里的异常,全局异常无法处理,比较高级没有到达 方法,所以需要自己处理 try-catch
*/
@Slf4j
public class ShiroAccessControlFilter extends AccessControlFilter {
/**
* 是否 允许 访问下一层
* true: 允许,交下一个Filter 处理
* false: 交给自己处理,往下执行 onAccessDenied 方法
* @param servletRequest
* @param servletResponse
* @param o
* @return
* @throws Exception
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
return false;
}
/**
* 表示访问拒绝时是否自己处理,
* 如果返回 true 表示自己不处理且 继续拦截器执行,往下执行
* 返回 false 表示自己已经处理了(比如重定向到另一个界面)处理完毕。
*
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request= (HttpServletRequest) servletRequest;
try {
log.info("接口请求方式{}",request.getMethod());
log.info("接口请求地址",request.getRequestURI());
String token=request.getHeader(Constant.TOKEN_SESSION_ID);
if(StringUtils.isEmpty(token)){
throw new BusinessException(4010001,"用户凭证已失效请重新登录认证");
}
ShiroUsernamePasswordToken customUsernamePasswordToken=new ShiroUsernamePasswordToken(token);
getSubject(servletRequest,servletResponse).login(customUsernamePasswordToken);
} catch (BusinessException e) {
customResponse(e.getMessageCode(),e.getMessage(),servletResponse);
return false;
} catch (AuthenticationException e) {
if(e.getCause() instanceof BusinessException){
BusinessException businessException= (BusinessException) e.getCause();
customResponse(businessException.getMessageCode(),businessException.getMessage(),servletResponse);
}else {
customResponse(4000001,"用户认证失败",servletResponse);
}
return false;
}catch (Exception e){
customResponse(5000001,"系统异常",servletResponse);
return false;
}
return true;
}
/**
* 异常处理
* 因为这里的位置是高于业务层的,所以这里的异常只能通过流的形式输出到前端。
* @param code
* @param msg
* @param response
*/
private void customResponse(int code, String msg, ServletResponse response) {
// 自定义异常的类,用户返回给客户端相应的JSON格式的信息
try {
Map<String, Object> result = new HashMap<>();
result.put("code", code);
result.put("msg", msg);
response.setContentType("application/json; charset=utf-8");
response.setCharacterEncoding("UTF-8");
String userJson = JSON.toJSONString(result);
// 写入到 流中,返回到客户端
OutputStream out = response.getOutputStream();
out.write(userJson.getBytes(StandardCharsets.UTF_8));
out.flush();
} catch (IOException e) {
log.error("eror={}", e.getLocalizedMessage());
}
}
}
3. ShiroUsernamePasswordToken
package com.feng.shiro;
import org.apache.shiro.authc.UsernamePasswordToken;
public class ShiroUsernamePasswordToken extends UsernamePasswordToken {
private String token;
public ShiroUsernamePasswordToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
4. ShiroRealm
package com.feng.shiro;
import com.feng.bean.SysUser;
import com.feng.service.PermissionService;
import com.feng.service.RoleService;
import com.feng.service.UserService;
import com.feng.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@Slf4j
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Resource
private RoleService roleService;
@Resource
private PermissionService permissionService;
@Autowired
private RedisUtil redisUtil;
/**
* 设置支持令牌校验
*
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof ShiroUsernamePasswordToken;
}
/**
* 授权
* 主要业务:
* 系统业务出现要验证用户的角色权限的时候,就会调用这个方法
* 来获取该用户所拥有的角色/权限
* 这个用户授权的方法我们可以缓存起来不用每次都调用这个方法。
* 后续的课程我们会结合 redis 实现它
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("ShiroRealm.doGetAuthorizationInfo()");
String token= (String) principalCollection.getPrimaryPrincipal();
String userId= (String) redisUtil.get(token);
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
//返回该用户的 角色信息 给授权器
List<String> roleNames = roleService.getRoleNamesByUserId(userId);
if (null != roleNames && !roleNames.isEmpty()) {
info.addRoles(roleNames);
}
//返回该用户的 权限信息 给授权器
Set<String> permissionPerms = permissionService.getPermissionPermsByUserId(userId);
if (permissionPerms != null) {
info.addStringPermissions(permissionPerms);
}
return info;
}
/**
* 认证
* 主要业务:
* 当业务代码调用 subject.login(customPasswordToken); 方法后
* 就会自动调用这个方法 验证用户名/密码
* 这里我们改造成 验证 token 是否有效 已经自定义了 shiro 验证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("ShiroRealm.doGetAuthenticationInfo()");
ShiroUsernamePasswordToken token = (ShiroUsernamePasswordToken) authenticationToken;
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo((String)token.getPrincipal(), (String)token.getCredentials(), ShiroRealm.class.getName());
return info;
}
private List<String> getRoleByUserId(String userId){
List<String> roles=new ArrayList<>();
if(userId.equals("8a938151-53e6-4182-925a-684f3be840e8")){
roles.add("admin");
}
roles.add("test");
return roles;
}
private List<String> getPermissionsByUserId(String userId){
List<String> permissions=new ArrayList<>();
if(userId.equals("8a938151-53e6-4182-925a-684f3be840e8")){
permissions.add("*");
}
permissions.add("sys:user:detail");
permissions.add("sys:user:edit");
return permissions;
}
}
5. ShiroHashedCredentialsMatcher
public class ShiroHashedCredentialsMatcher extends HashedCredentialsMatcher {
@Autowired
private RedisUtil redisUtil;
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
ShiroUsernamePasswordToken shiroUsernamePasswordToken= (ShiroUsernamePasswordToken) token;
String accessToken = (String) shiroUsernamePasswordToken.getPrincipal();
if(!redisUtil.hasKey(accessToken)){
throw new BusinessException(4001002,"授权信息信息无效请重新登录");
}
return true;
}
}
三、业务逻辑控制层代码
package com.feng.controller;
import com.feng.bean.SysUser;
import com.feng.service.UserService;
import com.feng.vo.LoginReqVO;
import com.feng.vo.LoginRespVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/user")
@Api(tags = "用户模块",description = "用户模块相关接口")
public class LoginController {
@Autowired
private UserService userService;
@GetMapping("/page")
public String index() {
return "login";
}
/**
* 前端用表单发请求如果使用 form-data、x-www-form-urlencoded 获取 ,则不可用 @RequestBody接受(因为他接受的为json)
* @param loginReqVO
* @return
*/
@ApiOperation(value = "用户登录接口")
@PostMapping(value = "/login")
@ResponseBody
public Map<String, Object> loginUser(@RequestBody LoginReqVO loginReqVO) {
LoginRespVO info = userService.login(loginReqVO);
Map<String, Object> result = new HashMap<>();
result.put("code", 0);
result.put("data", info);
return result;
}
@ApiOperation(value = "获取用户详情接口")
@GetMapping("/getuser/{id}")
@RequiresPermissions("sys:user:detail")
public Map<String, Object> getUserAllInfo(@PathVariable("id") String id){
Map<String, Object> result = new HashMap<>();
SysUser detail = userService.detail(id);
result.put("code", 0);
result.put("data", detail);
return result;
}
@GetMapping("/test")
public Map<String, Object> test(){
Map<String, Object> result = new HashMap<>();
result.put("code", 0);
result.put("data", "sucess");
return result;
}
}
五、postman测试和debug分析
1. 登录
登录的URL 在 ShiroConfig.shiroFilterFactoryBean()
方法中已经被设置成了白名单,在shiro的各处打了断电,也不会进入。直接进入到控制器层返回用户id和token。
http://localhost:8082/user/login
{
"username": "feng",
"password": "666666"
}
2. 获取用户信息
此URL 已经被设置走自定义的 ShiroAccessControlFilter
过滤器。
所以debug发请求分析如下:
- 先进入
ShiroAccessControlFilter.isAccessAllowed()
方法 - 进入到源码 AccessControlFilter.onPreHandle() 方法,这个方法会调用上面的方法和下面的方法
- 在进入到
ShiroAccessControlFilter.onAccessDenied()
方法,在这里获取token,并简单验证token是否存在,然后进行shiro的主体登录
。(一会儿,还会返回来) - 主体登录后,debug 跳转到
ShiroRealm.doGetAuthenticationInfo()
进行认证。 - 然后流转到 自定义的核心验证类和方法:
ShiroHashedCredentialsMatcher.doCredentialsMatch()
方法 。 - 然后返回到第三步 的
ShiroAccessControlFilter.onAccessDenied()
方法 的最后一行返回值,返回值为 true。 - 然后流转到
ShiroRealm.doGetAuthorizationInfo()
进行授权。这里进入到授权方法是因为在控制器方法上有注解:@RequiresPermissions("sys:user:detail")
http://localhost:8082/user/getuser/8a938151-53e6-4182-925a-684f3be840e8
六、注意的点
1. 数据库表的设计
用户登录只涉及到 user 表
shiro的权限授权部分涉及到 role、permission、user_role、role_permission
表。
2. shiro的认证授权流程
一定要多分析下shiro的认证流程,代码执行流程的走向。