Shiro这个框架呢,对我来说真的相见恨晚,一直想用,但没机会用。
这个Shiro我也不多做介绍了,本文也只是简单应用,供大家学习参考。
本文有点长,我写了小半天..有点耐心小伙子!
本文涉及Shiro知识点:自定义Realms,Shiro加密,登录验证,权限验证,前端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>
到此就结束咯!
对你有帮助的话,右上角给个赞呗~