SpringBoot integrates Shiro, authentication and authorization, global exception handling

Project version:

Official website configuration: shiro.apach.org/spring-boot.html

springboot2.x
shiro:1.5.3

Maven configuration:

		 <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.5.3</version>
        </dependency>

Integrate the core of shiro configuration:

Integrating shiro in springboot requires two classes: shiroConfig class and CustonRealm class.

ShiroConfig class:
As the name suggests, it is some configuration of shiro, compared to the previous xml configuration. Including: filtered files and permissions, password encryption algorithms, annotations and other related functions.

CustonRealm class:
Customize the Realm class to inherit the (AuthorizingRealm) class and override the methods in the parent class. There are two methods: doGetAuthorizationInfo (authority related) and doGetAuthenticationInfo (identity authentication).

Basic configuration:

shiroConfig configuration:

package com.cy.pj.common.config;

import com.cy.pj.sys.service.realm.ShiroUserRealm;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.mgt.RememberMeManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @Configuration 注解描述的类为一个配置对象,
 * 此对象也会交给spring管理
 */

@Configuration
public class SpringShiroConfig {
    
    


    //配置自定义的Realm
    @Bean
    public ShiroUserRealm shiroUserRealm() {
    
    
        ShiroUserRealm shiroUserRealm = new ShiroUserRealm();
        return shiroUserRealm;
    }
    
    /**
     * 配置advisor对象,开启@RequirePermission注解的配置,进行权限控制
     * 在需要进行授权访问的业务层(Service)方法上,添加执行此方法需要的权限标识,参考代码@RequiresPermissions(“sys:user:update”)
     *
     * @param securityManager
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
    
    
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }


    /**
     * 配置shiro中缓存管理器对象 (对象的名字不能写cacheManager,因为spring容器中已经存在一个名字为cacheManager的对象了)
     *
     * 作用:缓存授权信息,当第一次访问授权时,会调用自定义Realm的获取授权数据的方法,从数据库中查询授权数据,并将其以登录者的Principal为键,存储在缓存中。
     * 以后的每次访问授权,就直接从缓存中获取,而不再从数据库中获取。
     */
    @Bean
    public CacheManager shiroCacheManager() {
    
    

        return new MemoryConstrainedCacheManager();
    }

    /**
     * 配置记住我管理对象:底层同cookie对象将用户信息写到客户端
     */
    @Bean
    public RememberMeManager rememberMeManager() {
    
    
        CookieRememberMeManager cManager = new CookieRememberMeManager();
        //配置cookie
        SimpleCookie cookie = new SimpleCookie("rememberMe");
        //设置cookie执行时长(7天)
        cookie.setMaxAge(7 * 24 * 60 * 60);
        cManager.setCookie(cookie);
        return cManager;

    }


    //配置安全管理器
    @Bean
    public SecurityManager securitManager(Realm realm,
                                          CacheManager cacheManager,
                                          RememberMeManager rememberMeManager) {
    
    
        DefaultWebSecurityManager sManager = new DefaultWebSecurityManager();
        //注入realm对象
        sManager.setRealm(realm);
        //注入缓存管理器对象
        sManager.setCacheManager(cacheManager);
        //注入记住我管理对象
        sManager.setRememberMeManager(rememberMeManager);
        return sManager;
    }

    //配置过滤器工厂
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager) {
    
    
        ShiroFilterFactoryBean sfBean = new ShiroFilterFactoryBean();
        //设置权限管理器
        sfBean.setSecurityManager(securityManager);
        //设置认证页面
        sfBean.setLoginUrl("/doLoginUI");
        sfBean.setSuccessUrl("/doLoginUI");
        //定义map指定请求过滤规则(哪些资源允许匿名访问,哪些必须认证访问)
        Map<String, String> map = new LinkedHashMap<>();
        //静态资源允许匿名访问:"anon"
        map.put("/bower_components/**", "anon");
        map.put("/build/**", "anon");
        map.put("/dist/**", "anon");
        map.put("/plugins/**", "anon");
        map.put("/user/doLogin", "anon");
        map.put("/doLogout", "logout");  //logout对应着shiro框架中退出过滤器
        //除了匿名访问的资源,其它都要认证("authc")后访问
        // map.put("/**", "authc");    //假如需要实现记住我功能,这里的过滤器标识使用user
        map.put("/**", "user");
        sfBean.setFilterChainDefinitionMap(map);
        return sfBean;
    }
}


shiroConfig is not complicated, there are basically three methods. Before talking about these three methods, I would like to tell you about the three core concepts of Shiro:

1.Subject: 代表当前正在执行操作的用户,但Subject代表的可以是人,也可以是任何第三方系统帐号。当然每个subject实例都会被绑定到SercurityManger上。
2.SecurityManger:SecurityManager是Shiro核心,主要协调Shiro内部的各种安全组件,这个我们不需要太关注,只需要知道可以设置自定的Realm。
3.Realm:用户数据和Shiro数据交互的桥梁。比如需要用户身份认证、权限认证。都是需要通过Realm来读取数据。

shiroFilter method:
You can tell by the name of this method: shiro's filter can set the login page (setLoginUrl), insufficient permission jump page (setUnauthorizedUrl), permission control or identity authentication of specific pages.
Note: SecurityManager (setSecurityManager) needs to be set here.
The default filters include anno, authc, authcBasic, logout, noSessionCreation, perms, port, rest, roles, ssl, and user filters.
For details, you can check the package org.apache.shiro.web.filter.mgt.DefaultFilter. This class is commonly used for authc and anno.
securityManager method:
Looking at the source code, you can know that securityManager is an interface class. We can take a look at its implementation class:
Insert image description here.

Interested students can take a look at how it is implemented. Since the project is a web project, we use DefaultWebSecurityManager and then set up our own Realm.
ShiroUserRealm method:
Leave the instantiation of ShiroUserRealm to spring for management. Of course, you can also use annotations to inject it here.

ShiroUserRealm configuration:

package com.cy.pj.sys.service.realm;

import com.cy.pj.sys.mapper.SysMenuMapper;
import com.cy.pj.sys.mapper.SysRoleMenusMapper;
import com.cy.pj.sys.mapper.SysUserDeptMapper;
import com.cy.pj.sys.mapper.SysUserRoleMapper;
import com.cy.pj.sys.pojo.SysUser;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationException;
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.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.HashSet;
import java.util.List;

@Component
public class ShiroUserRealm extends AuthorizingRealm {
    
    
    @Autowired
    private SysUserDeptMapper sysUserDeptMapper;
    @Autowired
    private SysUserRoleMapper sysUserRoleMapper;
    @Autowired
    private SysRoleMenusMapper sysRoleMenusMapper;
    @Autowired
    private SysMenuMapper sysMenuMapper;

    /**
     * 负责授权信息的获取和封装
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
    
        System.out.println("访问数据库查询授权标识");
        //1.获取登录用户
        SysUser user = (SysUser) principalCollection.getPrimaryPrincipal();
        //2.基于用户id查询角色id并校验
        Integer[] roleIds = sysUserRoleMapper.findRolesByUserId(user.getId());
        if (roleIds == null || roleIds.length == 0) {
    
    
            throw new AuthorizationException("没有权限");
        }

        //3.基于角色id查询菜单id并校验
        List<Integer> menuIdS = sysRoleMenusMapper.findMenuIdsByRoleIds(roleIds);
        if (menuIdS == null || menuIdS.size() == 0) {
    
    
            throw new AuthorizationException();
        }
        //4.基于菜单id查询授权标示并校验
        Integer[] s = {
    
    };
        List<String> permission = sysMenuMapper.findPermissionById(menuIdS.toArray(s));
        if (permission == null || permission.size() == 0) {
    
    
            throw new AuthorizationException();
        }
        //5.封装查询结果并返回
        HashSet<String> set = new HashSet<>(); //有可能角色的权限会重复,所有需要存入set集合进行去重
        for (String per : permission) {
    
    
            if (!StringUtils.isEmpty(per)) {
    
    
                set.add(per);
            }
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(set);
        return info;//返回给授权管理器
    }

    /**
     * 负责认证信息的获取和封装
     */

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    
    
        //1.获取登录时输入的账户信息(从token中获取)
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        //2.基于用户名查询用户信息
        SysUser user = sysUserDeptMapper.findUserByUserName(username);
        //3.判定用户是否存在
        if (user == null) {
    
    
            throw new UnknownAccountException("用户不存在");
        }
        //4.判定用户是否已被禁用。(被禁用不允许登陆)
        if (user.getValid() == 0) {
    
    
            throw new LockedAccountException("账户被禁用");
        }
        //5.封装认证信息并返回
        ByteSource credentialsSalt = ByteSource.Util.bytes(user.getSalt());
        //记住:构建什么对象要看方法的返回值
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
                user,//principal (身份)
                user.getPassword(),//hashedCredentials已加密的密码
                credentialsSalt, //credentialsSalt 盐
                this.getName());//realName
        return info;//返回给授权管理器
    }

    /**
     * 设置凭证匹配器(与用户添加操作使用相同的加密算法)
     * //用户添加  shiro框架MD5加密
     * String newPassword = new SimpleHash(
     * "MD5",                  //algorithmName 算法
     * password,               //原密碼
     * salt,                   //盐
     * 1                       //hashIterations表示加密次数
     * ).toHex();                      //转16进制
     * <p>
     * 也可以用set方法设置加密
     */
    @Override
    public CredentialsMatcher getCredentialsMatcher() {
    
    
        //构建加密匹配器对象
        HashedCredentialsMatcher cMatcher = new HashedCredentialsMatcher();
        //设置加密算法
        cMatcher.setHashAlgorithmName("MD5");
        //设置加密次数
        cMatcher.setHashIterations(1);
        return cMatcher;
    }
}


Note:
The custom Realm class inherits the AuthorizingRealm class and overloads the doGetAuthorizationInfo and doGetAuthenticationInfo methods.
doGetAuthorizationInfo: Authorization authentication, that is, after logging in, each identity is not necessarily different, and the corresponding pages that can be viewed are also different.
doGetAuthenticationInfo: Identity authentication. That is, logging in verifies the identity information of the logging in person through the account number and password.

Controller class:
Create a new HomeIndexController class and add the following code:

 @RequestMapping(value = "/login", method = RequestMethod.GET)
    @ResponseBody
    public String defaultLogin() {
    
    
        return "首页";
    }

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    @ResponseBody
    public String login(@RequestParam("username") String username, @RequestParam("password") String password) {
    
    
        // 从SecurityUtils里边创建一个 subject
        Subject subject = SecurityUtils.getSubject();
        // 在认证提交前准备 token(令牌)
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        // 执行认证登陆
        try {
    
    
            subject.login(token);
        } catch (UnknownAccountException uae) {
    
    
            return "未知账户";
        } catch (IncorrectCredentialsException ice) {
    
    
            return "密码不正确";
        } catch (LockedAccountException lae) {
    
    
            return "账户已锁定";
        } catch (ExcessiveAttemptsException eae) {
    
    
            return "用户名或密码错误次数过多";
        } catch (AuthenticationException ae) {
    
    
            return "用户名或密码不正确!";
        }
        if (subject.isAuthenticated()) {
    
    
            return "登录成功";
        } else {
    
    
            token.clear();
            return "登录失败";
        }
    }

test:

We can use postman for testing:
Insert image description here
ok. Identity authentication is no problem. Let's consider how to add permissions.

Shiro exception handling can be encapsulated into global exception handling

package com.cy.pj.common.web;

import com.cy.pj.common.vo.JsonResult;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandling {
    
    
    /**
     * 处理shiro框架异常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(ShiroException.class)
    public JsonResult doHandleShiroException(
            ShiroException e) {
    
    
        JsonResult r = new JsonResult();
        r.setState(0);
        if (e instanceof UnknownAccountException) {
    
    
            r.setMessage("账户不存在");
        } else if (e instanceof LockedAccountException) {
    
    
            r.setMessage("账户已被禁用");
        } else if (e instanceof IncorrectCredentialsException) {
    
    
            r.setMessage("密码不正确");
        } else if (e instanceof AuthorizationException) {
    
    
            r.setMessage("没有此操作权限");
        } else {
    
    
            r.setMessage("系统维护中");
        }
        e.printStackTrace();
        return r;
    }

  //定义全局异常处理
    @ExceptionHandler(value = RuntimeException.class)
    public JsonResult topException(RuntimeException e) {
    
    
        e.printStackTrace();
        return new JsonResult(e);
    }

}

The user information obtained for login input can be encapsulated into an API

package com.cy.pj.common.util;

import com.cy.pj.sys.pojo.SysUser;
import org.apache.shiro.SecurityUtils;

public class ShiroUtils {
    
    

    //返回登陆用户信息
    public static SysUser getUser(){
    
    
      return (SysUser) SecurityUtils.getSubject().getPrincipal();
    }
    //返回登陆用户名
    public static   String getUsername(){
    
    
        return getUser().getUsername();
    }
}

Use annotations to configure permissions:

In fact, we can configure permissions without annotations, because we have added them before: the DefaultFilter class has perms (similar to perms[user:add]). But just imagine, the granularity of this control may be very fine, specific to the methods in a certain class. So if it is configured in a configuration file, does each method need to be added with a perms? But the annotation is different. It is written directly on the method, which is simple and fast.
It's very simple. You need to add the following code to the config class to enable annotations:

@Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    
    
        return new LifecycleBeanPostProcessor();
    }
    
 /**
     * *
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * *
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * * @return
     */
    @Bean
    @DependsOn({
    
    "lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
    
    
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
    
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
    
    
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

Create a new UserController class. as follows:

@RequestMapping("/user")
@Controller
public class UserController {
    
    
    @RequiresPermissions("user:list")
    @ResponseBody
    @RequestMapping("/show")
    public String showUser() {
    
    
        return "这是学生信息";
    }
}

Repeat the login steps just now. After the login is successful, postman enters localhost:8080/user/show
Insert image description here

There is indeed no authority. The method is @RequiresPermissions("user:list"), while in customRealm it is user:show, user:admin. We can adjust the permissions on the method to user:show. After debugging it, I found it was successful.
There is a problem here: when there is no permission, the system will report an error without jumping to the corresponding page without permission, that is, the setUnauthorizedUrl method does not work. The next article will give a solution to this problem -. -

Guess you like

Origin blog.csdn.net/m0_49353216/article/details/108991300