JavaWeb 权限管理设计与实现
- 序言
- 项目准备
- 项目结构
- Shiro配置
- 表单提交与ajax请求对于权限成功、失败的处理
- 前端页面按钮权限的隐藏与显示
序言
在开发过程中,权限是个很重要的模块,我们在做权限管理的时候采用的按角色分配权限,首先要设计好用户–角色–资源–权限这四者间的关系,接着要考虑前后台权限的控制,然后要确定好实现方式,现在主流的权限管理一个是Spring security,一个是shiro。前者功能强大,但是配置复杂,不易上手,后者功能简单,配置也很简单,所以此次记录下shiro的使用。
项目准备
1、spring boot 2.2.6.RELEASE
2、mybatis plus
3、mysql
4、shiro
5、freemarker
前端框架采用的是layui
项目结构
pom.xml
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<spring.boot.version>2.2.6.RELEASE</spring.boot.version>
<apache.commons.lang3.version>3.10</apache.commons.lang3.version>
<mysql.java.version>8.0.16</mysql.java.version>
<mybatis-plus.version>3.3.1</mybatis-plus.version>
<fastjson.version>1.2.68</fastjson.version>
<druid.spring.boot.version>1.1.20</druid.spring.boot.version>
<lombok.version>1.18.12</lombok.version>
<shiro.version>1.5.2</shiro.version>
<shiro-freemarker-tags.versoin>1.0.1</shiro-freemarker-tags.versoin>
</properties>
<dependencies>
<dependency>
<groupId>com.yml.easy</groupId>
<artifactId>easy-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
</dependency>
<dependency>
<groupId>net.mingsoft</groupId>
<artifactId>shiro-freemarker-tags</artifactId>
</dependency>
</dependencies>
application.yml
spring:
application:
name: so-easy
freemarker:
enabled: true
template-loader-path: classpath:/templates/
suffix: .html
charset: utf-8
content-type: text/html
cache: false
datasource:
druid:
url: jdbc:mysql://localhost:3306/so-easy?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
Shiro配置
其实配置shiro很简单,最主要的就是要两个类,EasyUserRealm
和ShiroConfig
,因为在实际运用中,项目中都有自己的密码验证实现,所以,这里又写了一个密码验证类EasyCredentialsMatcher
。EasyUserRealm.java
package com.yml.easy.config.shiro;
import com.alibaba.fastjson.JSONObject;
import com.yml.easy.entity.SysUserEntity;
import com.yml.easy.service.SysUserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* @author: YML
* @create: 2020-04-18 14:30
*/
@Component
public class EasyUserRealm extends AuthorizingRealm {
private Logger logger = LoggerFactory.getLogger(getClass());
@Resource
private SysUserService sysUserService;
/**
* 权限认证
* @author YML
* @date 2020/4/18 16:14
* @param principals principals
* @return org.apache.shiro.authz.AuthorizationInfo
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SysUserEntity user = (SysUserEntity) SecurityUtils.getSubject().getPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 权限
Set<String> permissions = new HashSet<>();
//查询角色
//查询权限(实际是从数据库查询)
permissions.addAll(Arrays.asList("admin","dev"));
authorizationInfo.setStringPermissions(permissions);
return authorizationInfo;
}
/**
* 登录验证
* @author YML
* @date 2020/4/18 14:32
* @param token token
* @return org.apache.shiro.authc.AuthenticationInfo
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
logger.info("执行登录认证:{}", JSONObject.toJSONString(token));
String username = (String) token.getPrincipal();
//获取数据库用户信息SysUserEntity为自定义的数据库映射 bean
SysUserEntity userEntity = sysUserService.queryUserByLoginOrPhone(username).getData();
if (userEntity == null) {
// 账号不存在
throw new UnknownAccountException("账户不存在");
}
//判断状态
if (userEntity.getUserStatus()!=1){
// 账号被锁定
throw new LockedAccountException("用户不可用");
}
String salt = userEntity.getUserSlat();
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userEntity,
userEntity.getUserPass(),
ByteSource.Util.bytes(salt),
getName());
return authenticationInfo;
}
/**
* 设置自定义密码验证
* @author YML
* @date 2020/4/18 16:14
* @param credentialsMatcher credentialsMatcher
*/
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
super.setCredentialsMatcher(new EasyCredentialsMatcher());
}
}
ShiroConfig
package com.yml.easy.config.shiro;
import com.jagregory.shiro.freemarker.ShiroTags;
import freemarker.template.TemplateException;
import org.apache.shiro.mgt.SessionsSecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import javax.annotation.Resource;
import java.io.IOException;
/**
* shiro配置
*
* @author: YML
* @create: 2020-04-18 14:32
*/
@Configuration
public class ShiroConfig {
@Resource
private EasyUserRealm easyUserRealm;
/**
* 配置URL过滤器
* @author YML
* @date 2019/11/3 20:43
* @return org.apache.shiro.spring.web.config.ShiroFilterChainDefinition
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/layui/**","anon");
chainDefinition.addPathDefinition("/css/**", "anon");
chainDefinition.addPathDefinition("/js/**","anon");
chainDefinition.addPathDefinition("/images/**", "anon");
chainDefinition.addPathDefinition("/json/**", "anon");
chainDefinition.addPathDefinition("/plugins/**", "anon");
// all other paths require a logged in user
chainDefinition.addPathDefinition("/","anon");
chainDefinition.addPathDefinition("/login","anon");
chainDefinition.addPathDefinition("/logout","logout");
chainDefinition.addPathDefinition("/sysUser/login","anon");
chainDefinition.addPathDefinition("/**", "user");
return chainDefinition;
}
/**
* 配置security并设置userReaml,避免xxxx required a bean named 'authorizer' that could not be found.的报错
* @author YML
* @date 2019/11/3 20:44
* @return org.apache.shiro.mgt.SessionsSecurityManager
*/
@Bean
public SessionsSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(easyUserRealm);
return securityManager;
}
/**
* 开启Shiro的注解,需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
* @author YML
* @date 2019/11/3 20:45
* @return org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator
*/
@Bean
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;
}
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() throws IOException, TemplateException {
FreeMarkerConfigurer freeMarkerConfigurer = new FreeMarkerConfigurer();
freeMarkerConfigurer.setTemplateLoaderPath("classpath:templates/");
freemarker.template.Configuration configuration = freeMarkerConfigurer.createConfiguration();
configuration.setDefaultEncoding("UTF-8");
//这里可以添加其他共享变量 比如sso登录地址
configuration.setSharedVariable("shiro", new ShiroTags());
freeMarkerConfigurer.setConfiguration(configuration);
return freeMarkerConfigurer;
}
}
对于shiroFilterChainDefinition()
方法中对Url进行过滤时,第二个参数代表着过滤条件,具体可见如下
EasyCredentialsMatcher
package com.yml.easy.config.shiro;
import com.yml.easy.util.EncryptionUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* shiro认证密码比较
*
* @author: YML
* @create: 2020-04-18 15:05
*/
public class EasyCredentialsMatcher implements CredentialsMatcher {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 密码校验
* @author YML
* @date 2020/4/18 15:05
* @param authToken 登录信息
* @param info 用户信息
* @return boolean
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken authToken, AuthenticationInfo info) {
logger.info("密码校验。。。");
UsernamePasswordToken token = (UsernamePasswordToken) authToken;
String tokenPass = String.valueOf(token.getPassword());
SimpleAuthenticationInfo authenticationInfo = (SimpleAuthenticationInfo) info;
String password = authenticationInfo.getCredentials().toString();
String slat = new String(authenticationInfo.getCredentialsSalt().getBytes());
return EncryptionUtils.md5Check(password,tokenPass,slat);
}
}
然后在写个controller
@RestController
@RequestMapping("/sysUser/")
public class SysUserController {
@Resource
private SysUserService sysUserService;
/**
* 用户登录
* @author YML
* @date 2020/4/18 15:38
* @param entity entity
* @param request request
* @return com.yml.easy.meta.CallResult<java.util.Map<java.lang.String,java.lang.Object>>
*/
@RequestMapping("login")
public CallResult<Map<String,Object>> login(SysUserEntity entity, HttpServletRequest request){
CallResult<Map<String,Object>> callResult = CallResult.newInstance();
if (null==entity || ParamUtils.isEmpty(entity.getUserLogin()) || ParamUtils.isEmpty(entity.getUserPass())){
callResult.setBaseResponse(BaseResponse.LOSE_USERNAME_PASSWORD);
return callResult;
}
Subject sub = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(entity.getUserLogin(), entity.getUserPass());
try {
sub.login(token);
return callResult;
} catch (UnknownAccountException e) {
return CallResult.newInstance(BaseResponse.USER_NOT_EXIST);
}
catch (LockedAccountException e) {
return CallResult.newInstance(BaseResponse.NO_LOGIN_PERMISSION);
}catch (IncorrectCredentialsException ice) {
return CallResult.newInstance(BaseResponse.PASSWORD_ERROR);
}
}
}
至此,后台的配置就已经完成了,对于后台的请求方法权限,我们可以通过注解来进行,shiro常用的注解如下:
按照当前的配置,常用的就是@RequiresPermissions
,如
@RequestMapping("login")
@RequiresPermissions({"admin"})
public void addUser(){
}
表单提交与ajax请求对于权限成功、失败的处理
通过spring的全局异常拦截进行处理
package com.yml.easy.handler;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import javax.servlet.http.HttpServletRequest;
/**
* 异常处理机制
*
* @author: YML
* @create: 2020-04-20 20:38
*/
@ControllerAdvice
public class EasyExceptionHandler {
@ExceptionHandler(AuthorizationException.class)
public Object authorizationExceptionHandler(AuthorizationException e, HttpServletRequest request) {
//根据请求类型进行转发
if (isAjax(request)){
return "redirect:/err/authorizationExceptionJson";
}else{
return "redirect:/err/authorizationExceptionPage";
}
}
/**
* 判断是否是Ajax请求
* @author YML
* @date 2020/4/20 21:07
* @param request request
* @return boolean
*/
public boolean isAjax(HttpServletRequest request) {
return (request.getHeader("X-Requested-With") != null &&
"XMLHttpRequest".equals(request.getHeader("X-Requested-With").toString()));
}
}
前端页面按钮权限的隐藏与显示