shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。

前言

一、逻辑

1. 登录逻辑

shiro 做安全权限控制。那么shiro的过滤器和数据源的处理主要是针对token的认证和授权。
而用户的密码验证则还是在service层中进行处理。

  1. 首先将用户登录的接口 /user/login 在shiro过滤器中放开,不在拦截,设置为 anon
  2. 发送post请求携带用户名和密码,进行登录。走这个请求时,在上一步已经被设置了白名单。走控制器,业务层service,在这里进行判断,用户名密码判断无误后,设置token和id,然后返回到前端。
  3. 总结一下:用户的密码登录和shiro是无关的,正常的判断即可。注意的是需要返回的数据,这里仅有id和token。token也是UUID生成的,并存在redis中。
  4. 访问其他接口时,拿着token放在请求头中,然后发请求,没有被shiro设置在白名单里的请求会被shiro拦截,拦截流程如下:
    ShiroAccessControlFilter类:isAccessAllowed():onAccessDenied(),用到了ShiroUsernamePasswordToken
    ->ShiroRealm类:doGetAuthenticationInfo方法
    ->ShiroHashedCredentialsMatcher类:doCredentialsMatch()
  5. 根据博客三,这里重写的doCredentialsMatch()方法就是 shiro的过滤器的最后一步,也是至关重要的一步。
    6. 接下来就可以去控制的接口了,如果遇到@RequiresPermissions() 注解,再去ShiroRealm类中doGetAuthorizationInfo()授权方法去授予权限即可,再去接口即可。

2. 项目目录结构

在这里插入图片描述

3. 开发逻辑

a. redis 开发工具类

redis的配置类和工具类,我就不再贴出,本博客主要写shiro的代码,代码可去GitHub上拉取下来观看

  1. config包下的RedisConfig配置类
  2. serializer包下的MyStringRedisSerializer序列化类
  3. utils包下的RedisUtil类

代码不再贴了,都在GitHub上

b. 密码加密工具类

  1. utils 包下的 PasswordUtils 工具类。
  2. PasswordEncoder密码编码类。

代码不再贴了,都在GitHub上

c. swagger配置

  1. config包下的 SwaggerConfig配置类
  2. 配置类上的启动注解 @EnableSwagger2

代码不再贴了,都在GitHub上

d. shiro开发 流程

  1. 先开发 ShiroConfig,设置自定义过滤器 ShiroAccessControlFilter,并设置过滤器的白名单
    对登录的请求设置为 anon。
  2. 开发 ShiroAccessControlFilter拦截器,这里是对 token 的简单过滤验证,并进行主体提交,提交到自定义数据域 realm。
    开发拦截器中,还要开发ShiroUsernamePasswordToken,自己实现shiro认证机制,就要重写类 UsernamePasswordToken
  3. 开发自定义realm:ShiroRealm类。数据域部分,进行授权和认证。
    认证:对token进行过滤,用户名密码登录部分还是在业务层处理
    授权:从数据库中获取,设计到表 role、permission、user_role、role_permission 四个表
  4. 开发 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发请求分析如下:

  1. 先进入 ShiroAccessControlFilter.isAccessAllowed() 方法
  2. 进入到源码 AccessControlFilter.onPreHandle() 方法,这个方法会调用上面的方法和下面的方法
  3. 在进入到 ShiroAccessControlFilter.onAccessDenied() 方法,在这里获取token,并简单验证token是否存在,然后进行 shiro的主体登录。(一会儿,还会返回来)
  4. 主体登录后,debug 跳转到 ShiroRealm.doGetAuthenticationInfo() 进行认证。
  5. 然后流转到 自定义的核心验证类和方法ShiroHashedCredentialsMatcher.doCredentialsMatch() 方法 。
  6. 然后返回到第三步 的 ShiroAccessControlFilter.onAccessDenied() 方法 的最后一行返回值,返回值为 true。
  7. 然后流转到 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的认证流程,代码执行流程的走向。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java冯坚持

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值