集成安全框架 - Shiro

说明:该案例只做了用户身份认证,并未做角色和权限授权认证

源码地址:

// shiro_demo 分支下
https://gitee.com/zhangbocong/spring_boot_demo.git

一. 添加 Maven 依赖

<!-- shiro -->
<dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring</artifactId>
      <version>1.6.0</version>
</dependency>
<!-- lang3 -->
<dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
</dependency>

二. 创建用户ServiceImpl(具体接口实现就不写了,不会请自行右上角)

import com.example.dao.PermissionsDao;
import com.example.dao.RoleDao;
import com.example.dao.UserDao;
import com.example.entity.UserEntity;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Set;

/**
 * @author :zhangbocong
 * @version :V1.0
 * @program :spring_boot_demo
 * @date :Created in  16:30
 * @description : 用户实现类
 */
@Service
public class LoginServiceImpl {
    @Resource
    UserDao userDao;
    @Resource
    RoleDao roleDao;
    @Resource
    PermissionsDao permissionsDao;

    /**
     * @Description 用户注册
     * @Param [userEntity 用户信息]
     * @Return java.lang.Boolean
     */
    public Boolean addUser(UserEntity userEntity){
        // 1. 验证用户是否存在(数据库验证,实际应使用redis)
        String user = userDao.getUser(userEntity.getUserName());
        if (StringUtils.isNotBlank(user)){
            return false;
        }
        // 2. 设置密码加密算法和加密次数(加密一次,与自定义realm中一致)
        Md5Hash md5Hash =new Md5Hash(userEntity.getPassword(),userEntity.getUserName());
        userEntity.setPassword(md5Hash.toString());
        return userDao.addUser(userEntity)>0;
    }

    /**
     * @Description 用户登录
     * @Param [getMapByName 用户名]
     * @Return com.example.entity.UserEntity 用户信息
     */
    public String getUserByName(String getMapByName) {
        return userDao.getUser(getMapByName);
    }

    /**
     * @Description 根据用户名获取用户角色
     * @Param [userName 用户名]
     * @Return java.util.List<java.lang.String> 角色名称集合
     */
    public Set<String> getRoleByUserName(String userName){
        return roleDao.getRoleByUserName(userName);
    }

    /**
     * @Description 根据角色名称获取角色权限
     * @Param [roleNames 角色名称集合]
     * @Return java.util.List<java.lang.String> 权限集合
     */
    public Set<String> getPermissionsByRoleNames(Set<String> roleNames){
        return permissionsDao.getPermissionsByRoleName(roleNames);
    }
}

三. 创建自定义Realm:CustomRealm

package com.example.shiro;

import com.example.service.LoginServiceImpl;
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.util.StringUtils;

import javax.annotation.Resource;
import java.util.Set;

/**
 * @author :zhangbocong
 * @version :V1.0
 * @program :spring_boot_demo
 * @date :Created in 2021年3月16日 9:10
 * @description :自定义Realm
 */
public class CustomRealm extends AuthorizingRealm {
    @Resource
    LoginServiceImpl loginService;

    /**
     * @Description 授权
     * @Param [principalCollection]
     * @Return org.apache.shiro.authz.AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 1. 获取用户名
        String userName = (String) principalCollection.getPrimaryPrincipal();
        // 2. 获取用户角色
        Set<String> roles = loginService.getRoleByUserName(userName);
        // 3. 获取角色权限
        Set<String> permissions = loginService.getPermissionsByRoleNames(roles);
        // 4. 创建返回对象
        SimpleAuthorizationInfo simpleAuthenticationInfo = new SimpleAuthorizationInfo();
        // 4.1 设置角色
        simpleAuthenticationInfo.setRoles(roles);
        // 4.2 设置权限
        simpleAuthenticationInfo.setStringPermissions(permissions);
        return simpleAuthenticationInfo;
    }

    /**
     * @Description 认证
     * @Param [authenticationToken]
     * @Return org.apache.shiro.authc.AuthenticationInfo
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1. 接收主体传过来的用户名
        String userName = (String) authenticationToken.getPrincipal();
        // 2. 获取用户密码
        String password = loginService.getUserByName(userName);
        if (StringUtils.isEmpty(password)) {
            return null;
        } else {
            // 3. 创建返回对象并设置用户信息
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(userName, password, getName());
            // 3.1 设置 盐
            simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(userName));
            return simpleAuthenticationInfo;
        }
    }
}

四. 创建ShiroConfig

package com.example.config;

import com.example.shiro.CustomRealm;

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
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.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;
/**
 * @author :zhangbocong
 * @version :V1.0
 * @program :spring_boot_demo
 * @date :Created in  16:54
 * @description :shiro 配置类
 */
@Configuration
public class ShiroConfig {


    /**
     * @Description 自定义凭证匹配器 配置密码比较器
     * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了所以我们需要修改下doGetAuthenticationInfo中的代码)
     * 可以扩展凭证匹配器,实现输入密码错误次数后锁定等功能
     * @Return org.apache.shiro.authc.credential.HashedCredentialsMatcher
     */
    @Bean(name = "credentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");//散列算法:这里使用MD5加密算法;
        hashedCredentialsMatcher.setHashIterations(1);//散列的次数:加密次数,比如散列两次,相当于 md5(md5(""));
        //storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
        // hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }

    /**
     * @Description 将自己的验证方式加入容器
     * @Return com.example.shiro.CustomRealm
     */
    @Bean
    public CustomRealm customRealm() {
        CustomRealm customRealm = new CustomRealm();
        customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return customRealm;
    }

    /**
     * @Description 权限管理,配置主要是Realm的管理认证
     * @Return org.apache.shiro.mgt.SecurityManager
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customRealm());
        return securityManager;
    }

    /**
     * @Description Filter工厂,设置对应的过滤条件和跳转条件
     * @Param [securityManager 安全管理器]
     * @Return org.apache.shiro.spring.web.ShiroFilterFactoryBean
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> map = new HashMap<>();
        //登出
        map.put("/logout", "logout");
        //对所有用户认证 value为"authc"
        map.put("/**", "authc");
        //不做验证的接口 value为"anon"
        map.put("/addUser","anon");
        //登录
        shiroFilterFactoryBean.setLoginUrl("/login");
        //首页
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //错误页面,认证不通过跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    /**
     * @Description 代理生成器:解决spring aop和注解配置一起使用的bug
     * 借助SpringAOP来扫描@RequiresRoles@RequiresPermissions等注解,生成代理类实现功能增强,从而实现权限控制。
     * @Return org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator
     */
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

    /**
     * @Description 需要DefaultAdvisorAutoProxyCreator一起使用,否则权限注解无效。
     * @Return org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

五. 创建异常类:MyExceptionHandler

package com.example.exception;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author :zhangbocong
 * @version :V1.0
 * @program :spring_boot_demo
 * @date :Created in  17:13
 * @description :异常类
 */
@ControllerAdvice
@Slf4j
public class MyExceptionHandler {
    @ExceptionHandler
    @ResponseBody
    public String ErrorHandler(AuthorizationException e) {
        log.error("没有通过权限验证!", e);
        return "没有通过权限验证!";
    }
}

六.创建测试类

package com.example.controller;

import com.example.entity.UserEntity;
import com.example.service.LoginServiceImpl;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author :zhangbocong
 * @version :V1.0
 * @program :spring_boot_demo
 * @date :Created in 2021年3月16日 9:22
 * @description :自定义Realm测试
 */
@RestController
public class CustomRealmTest {
    @Resource
    LoginServiceImpl loginService;

    /**
     * @Description 用户注册
     * @Param [userEntity 用户注册信息]
     * @Return java.lang.Boolean true成功 false用户名已存在
     */
    @RequestMapping("addUser")
    public Boolean addUser(@RequestBody UserEntity userEntity){
        return loginService.addUser(userEntity);
    }

    /**
     * @Description 用户登录
     * @Param [user 用户信息]
     * @Return java.lang.String 登录返回状态
     */
    @GetMapping("/login")
    public String login(UserEntity user) {
        // 1. 验证用户信息
        if (StringUtils.isEmpty(user.getUserName()) || StringUtils.isEmpty(user.getPassword())) {
            return "请输入用户名和密码!";
        }

        // 2. 获取主体
        Subject subject = SecurityUtils.getSubject();

        // 3. 获取token
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
                user.getUserName(),
                user.getPassword()
        );

        // 4. 进行身份验证,这里可以捕获异常,然后返回对应信息
        try {
            subject.login(usernamePasswordToken);
            subject.checkRoles("admin");
            subject.checkPermissions("query", "add");
        } catch (UnknownAccountException e) {
            return "用户名不存在!";
        } catch (AuthenticationException e) {
            return "密码错误!";
        } catch (AuthorizationException e) {
            return "权限不足,请联系管理员";
        }
        return "login success";
    }

    //验证用户是否带有admin角色
    @RequiresRoles("admin")
    @GetMapping("/admin")
    public String admin() {
        return "admin success!";
    }
    //验证用户是否带有user角色
    @RequiresRoles("user")
    @GetMapping("/user")
    public String user() {
        return "user success!";
    }
    //验证用户是否带有query权限
    @RequiresPermissions("query")
    @GetMapping("/index")
    public String index() {
        return "index success!";
    }
    //验证用户是否带有add权限
    @RequiresPermissions("add")
    @GetMapping("/add")
    public String add() {
        return "add success!";
    }
    //验证用户是否带有update权限
    @RequiresPermissions("update")
    @GetMapping("/update")
    public String update() {
        return "update success!";
    }
}

七. 测试