SpringBoot整合Shiro示例

Shiro这个框架呢,对我来说真的相见恨晚,一直想用,但没机会用。

这个Shiro我也不多做介绍了,本文也只是简单应用,供大家学习参考。

本文有点长,我写了小半天..有点耐心小伙子!

本文涉及Shiro知识点:自定义Realms,Shiro加密,登录验证,权限验证,前端Shiro标签,Shiro注解。

目录

Shiro的三大核心组件

代码示例

pom.xml

Entity

自定义Realm

登录接口

注册接口

配置ShiroConfig注册Bean

配置Shiro注解实现权限控制

前端配置Shiro标签,实现权限控制显示隐藏


Shiro的三大核心组件

  • Subject
  • SecurityManager
  • Realms

大白话简述

Subject:代表了与当前软件交互的用户

SecurityManager:管理所有的Subject,也是通过它来提供各种服务,可以理解为是Shiro核心的核心

Realms:代表了Subject与当前软件的桥梁,当Subject登录或者访问时,SecurityManager会从配置的Realms中对Subject进行授权认证,Realm允许配置多个,但是至少要有一个(我们接下来主要也是需要自定义Realm,来从DB或Cache中获取Subject的权限进行校验以及登录逻辑)

以上简述纯个人理解,如有错误,欢迎指出,谢谢!

代码示例

pom.xml

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<version>1.4.0</version>
</dependency>
<dependency>
	<groupId>com.github.theborakompanioni</groupId>
	<artifactId>thymeleaf-extras-shiro</artifactId>
	<version>2.0.0</version>
</dependency>

thymeleaf-extras-shiro

引入这个依赖是为了一会前端使用Shiro标签,如前后端分离,只提供restful接口则不需要。

Entity

Member

package com.p_job.chess.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@TableName("u_member")
public class Member {

    private Long id;
    private String name;
    private String phone;
    private String password;
    private String salt;
    private String question;
    private String answer;
    private String headImgUrl;
    private Integer roleId;
    private LocalDateTime updateTime;
    private LocalDateTime createTime;

}

Role

package com.p_job.chess.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@TableName("u_role")
public class Role {

    private Integer id;
    private String name;
    private Integer status;
    private Integer defaultRole;
    private LocalDateTime createTime;

}

Permission

package com.p_job.chess.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@TableName("u_permission")
public class Permission {

    private Integer id;
    private String name;
    private String url;
    private String permission;
    private Integer status;
    private LocalDateTime createTime;

}

RolePermission

package com.p_job.chess.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("u_role_permission")
public class RolePermission {

    private Integer roleId;
    private Integer permissionId;

}

RolePermissionInfo

package com.p_job.chess.entity;

import lombok.Data;

import java.util.List;

@Data
public class RolePermissionInfo {

    private Role role;
    private List<Permission> permissionList;

}

自定义Realm

ShiroRealm

package com.p_job.chess.shiro;

import com.p_job.chess.entity.Member;
import com.p_job.chess.entity.Permission;
import com.p_job.chess.entity.RolePermissionInfo;
import com.p_job.chess.service.MemberService;
import com.p_job.chess.service.RoleService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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 java.util.List;

public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    private MemberService memberService;

    @Autowired
    private RoleService roleService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        Object primaryPrincipal = principalCollection.getPrimaryPrincipal();
        if (primaryPrincipal == null) return null;
        Member member = (Member) primaryPrincipal;

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        RolePermissionInfo rolePermissionInfo = roleService
                .queryRolePermissionById(member.getRoleId());
        authorizationInfo.addRole(rolePermissionInfo.getRole().getName());
        List<Permission> permissionList = rolePermissionInfo.getPermissionList();
        for (int i = 0, size = permissionList.size(); i < size; i++){
            Permission permission = permissionList.get(i);
            authorizationInfo.addStringPermission(permission.getPermission());
        }
        return authorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String phone = (String) authenticationToken.getPrincipal();
        Member member = memberService.findByPhone(phone);
        if (member == null) return null;

        ByteSource source = ByteSource.Util.bytes(member.getSalt());
        // params: 1:认证的实体;2:加密后的密码;3:Byte随机盐;4:当前realm标识
        return new SimpleAuthenticationInfo(member, member.getPassword(), source, getName());
    }

}

大白话简述

doGetAuthorizationInfo

进行权限验证,在每次访问拦截的url时都会触发一次验证Subject的权限。

我们在这需要做的逻辑就是对当前访问的Subject,从DB中获取到他的角色权限添加到authorizationInfo认证信息中交给Shiro处理。

doGetAuthenticationInfo

进行登录验证,当Subject调用登录接口时触发该接口进行账密验证。

账密对应的参数名:username,password

登录接口

@PostMapping("login")
public void login(HttpServletRequest request) throws ServletException, IOException {
    String exception = (String)request.getAttribute("shiroLoginFailure");
    MemberEnum memberEnum = null;
    if (!StringUtils.isEmpty(exception)){
        if (UnknownAccountException.class.getName().equals(exception))
            memberEnum = MemberEnum.PHONE_NOT_EXIST;
        else if (IncorrectCredentialsException.class.getName().equals(exception))
            memberEnum = MemberEnum.PASSWORD_NOT_EQ;
    }
    if (memberEnum == null)
        throw new ShiroException(ResultEnum.FAIL_ERROR.getCode(), 
            ResultEnum.FAIL_ERROR.getMsg());
    else throw new ShiroException(memberEnum.getCode(), memberEnum.getMsg());
}

当Shiro登录验证失败时才会处理,所以这里我们只处理登录失败的情况。

当用户不存在,Shiro会抛UnknownAccountException异常

当密码错误,Shiro会抛IncorrectCredentialsException异常

我这里抛了一个自定义异常ShiroException,然后进行全局异常捕获,返回登录失败提示语

ShiroException

package com.p_job.chess.exception;

import lombok.NoArgsConstructor;

@NoArgsConstructor
public class ShiroException extends RuntimeException {

    private Integer code;
    private String message;

    public ShiroException(Integer code, String message){
        super(message);
        this.code = code;
        this.message = message;
    }

}

GlobalExceptionHandler

package com.p_job.chess;

import com.p_job.chess.enums.ResultEnum;
import com.p_job.chess.exception.MatchException;
import com.p_job.chess.exception.MemberException;
import com.p_job.chess.exception.ParamException;
import com.p_job.chess.exception.ShiroException;
import com.p_job.chess.properties.ShiroConfigYml;
import com.p_job.chess.utils.ResultVoUtil;
import com.p_job.chess.vo.ResultVo;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;

@ControllerAdvice
public class GlobalExceptionHandler {

    @Autowired
    private ShiroConfigYml shiroConfigYml;

    @ExceptionHandler(ShiroException.class)
    public void shiroExceptionHandler(HttpServletResponse response, ShiroException e) throws IOException {
        e.printStackTrace();
        String loginUrl = shiroConfigYml.getLogoutSuccessUrl();
        loginUrl += "?resetUrll="+loginUrl+"&failMsg=" + URLEncoder.encode(
                URLEncoder.encode(e.getMessage(), "utf-8"), "utf-8");
        response.sendRedirect(loginUrl);
    }

    @ResponseBody
    @ExceptionHandler(UnauthorizedException.class)
    public ResultVo shiroUnauthorizedException(UnauthorizedException e){
        ResultEnum noPermission = ResultEnum.NO_PERMISSION;
        return ResultVoUtil.getResultVo(noPermission.getCode(), noPermission.getMsg());
    }


}

shiroExceptionHandler

捕获我们自定义的ShiroException,将登录失败异常重定向到登录页提示。

shiroUnauthorizedException

捕获Shiro的UnauthorizedException异常,当Subject无权限访问时,Shiro将抛这个异常,这里捕获统一处理返回json提示“您没有权限访问”

注册接口

注册这里需要对Subject密码进行加密,这里加密规则需与Shiro的加密一致,在后面讲ShiroConfig时会看到

public boolean insert(Member member) {
    EntryptUtil.entryptPassword(member);
    return memberMapper.insert(member) == 1;
}
public static void entryptPassword(Member member){
    String salt = UUID.randomUUID().toString();
    Object md5Password = new SimpleHash(entryptUtil.shiroConfigYml.getAlgorithmName(),
            member.getPassword(), ByteSource.Util.bytes(salt),
                entryptUtil.shiroConfigYml.getHashIterations());
    member.setSalt(salt);
    member.setPassword(String.valueOf(md5Password));
}

加密方式跟次数这里塞配置文件了

配置ShiroConfig注册Bean

ShiroConfig(每个Bean作用可看注释)

package com.p_job.chess.shiro;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.p_job.chess.properties.ShiroConfigYml;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

@Configuration
public class ShiroConfig {

    @Autowired
    private ShiroConfigYml shiroConfigYml;

    /**
     * 配置shiro拦截器验证规则
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        String[] anonUrls = shiroConfigYml.getDefinitionMap().get("anon-urls");
        for (int i = 0, length = anonUrls.length; i < length; i++)
            filterChainDefinitionMap.put(anonUrls[i], "anon");
        // 退出登录
        filterChainDefinitionMap.put(shiroConfigYml.getLogoutUrl(), "logout");
        // 拦截其余所有请求
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        shiroFilterFactoryBean.setLoginUrl(shiroConfigYml.getLoginUrl());
        // 登录成功后跳转
        shiroFilterFactoryBean.setSuccessUrl(shiroConfigYml.getLoginSuccessUrl());
        // 验证失败地址
        shiroFilterFactoryBean.setUnauthorizedUrl(shiroConfigYml.getUnauthorizedUrl());
        return shiroFilterFactoryBean;
    }

    /**
     * 自动创建代理类
     */
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 自定义Realm
     */
    @Bean
    public ShiroRealm shiroRealm() {
        ShiroRealm itDragonShiroRealm = new ShiroRealm();
        itDragonShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return itDragonShiroRealm;
    }

    /**
     * 配置MD5加密规则
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName(shiroConfigYml.getAlgorithmName());
        hashedCredentialsMatcher.setHashIterations(shiroConfigYml.getHashIterations());
        return hashedCredentialsMatcher;
    }

    /**
     * 开启注解
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 配置shiro核心安全管理
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm());
        return securityManager;
    }

    /**
     * 使shiro标签生效
     * @return
     */
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }


}

shiroFilter登录那特别说明一下

shiroFilterFactoryBean.setLoginUrl(shiroConfigYml.getLoginUrl());

这里塞了登录的提交url,不是登录界面。

配置了这个之后前端提交登录接口,filter将拦截到去触发我们上面自定义的Realm执行doGetAuthenticationInfo

配置Shiro注解实现权限控制

注解介绍,摘自网络

https://www.cnblogs.com/pingxin/p/p00115.html

@RequiresAuthentication

表示当前Subject已经通过login 进行了身份验证;即Subject. isAuthenticated()返回true。

@RequiresUser

表示当前Subject已经身份验证或者通过记住我登录的。

@RequiresGuest

表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。

@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)

@RequiresRoles(value={“admin”})

@RequiresRoles({“admin“})

表示当前Subject需要角色admin 和user。

@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)

表示当前Subject需要权限user:a或user:b

示例

@PostMapping("upload-img")
@RequiresPermissions(value = {"match:add", "match:update"}, logical = Logical.OR)
public ResultVo uploadImg(@RequestParam("file") MultipartFile multipartFile) throws IOException {
    String matchFileDir = ioConfigYml.getMatchImgDir();
    String imgUrl = fileUtil.uploadFile(multipartFile, matchFileDir);
    return ResultVoUtil.getSuccessVo(imgUrl);
}

表示访问这个接口需要有"match:add"或者"match:update"标识的权限才可以访问。

在上述自定义Realm中的doGetAuthorizationInfo里,有一行

authorizationInfo.addStringPermission(permission.getPermission());

这里就是把权限的标识赋值给Subject

前端配置Shiro标签,实现权限控制显示隐藏

本文环境示例:前端文件:html,模板引擎:thymeleaf

引入Shiro标签库

<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

Shiro标签示例

<shiro:hasPermission name="match:add">
    <hr/>
    <button type='button' id="match-insert-btn" class='layui-btn oval-btn'>添加赛事</button>
</shiro:hasPermission>

到此就结束咯!

对你有帮助的话,右上角给个赞呗~

发布了61 篇原创文章 · 获赞 90 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/wkh___/article/details/104327153