文章预览
前言
安全框架就是解决系统安全的框架。如果没有安全框架,我们需要手动的处理每个资源的访问控制,这是非常麻烦的。使用了安全框架,我们可以通过配置的方式实现对资源的访问限制。
本篇博客将记录一个完整的前后端分离SpringSecurity+JWT的过程
涉及到的技术包括springboot、Mybatis-Plus、Redis、SpringSecurity以及JWT等
源码以及数据库文件已上传至码云https://gitee.com/niusongcun/myapp.git
一、数据库准备
对于权限操作,经典的就是五表查询即
user
、role
、user_role
、menu
、role_menu
具体的表结构可以根据自己的实际项目来修改,这里以我自己写的为例子
1.1、用户表
存储基本的用户信息
1.2、角色表
存储角色信息
1.3、用户角色表
存储用户与角色之间的关联信息即一个用户对应的角色
1.4、权限(菜单)表
存储菜单资源的信息即权限
1.5、角色权限表
存储角色与权限之间的关系即一个角色具有的权限
二、环境准备
2.1、整合Mybatis-Plus
2.1.1、导入依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 引入Druid依赖,阿里巴巴所提供的数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>
<!--Mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<!--mp代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.2.0</version>
</dependency>
<!--freemarker模板引擎用于代码生成 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- mybatis的分页助手-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.10</version>
<exclusions> <!--排除与mybatis-plus的冲突否则报错-->
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
</exclusions>
</dependency>
2.1.2、配置文件
server:
port: 8088
# DataSource Config
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/myapp?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
mybatis-plus:
mapper-locations: classpath*:/mapper/**Mapper.xml
2.1.3、配置Mybatis-Plus
开启mapper接口扫描,添加分页、防全表更新插件
package com.markerhub.config;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.zzuli.mapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
// 防止全表更新插件
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> configuration.setUseDeprecatedExecutor(false);
}
}
2.1.4、生成代码
有了数据库之后,那么现在就已经可以使用mybatis plus了,官方给我们提供了一个代码生成器,然后我写上自己的参数之后,就可以直接根据数据库表信息生成entity、service、mapper等接口和实现类。
然后我们单独运行CodeGenerator的main方法,注意调整CodeGenerator的数据库连接、账号密码啥的,然后我们输入表名称,通过逗号隔开:sys_menu,sys_role,sys_role_menu,sys_user,sys_user_role
生成以下代码
package com.zzuli;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
// gc.setOutputDir("D:\\test");
gc.setAuthor("关注公众号:One图云");
gc.setOpen(false);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
gc.setServiceName("%sService");
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/myapp?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("123456");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName(null);
pc.setParent("com.zzuli");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mapper/"
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix("m_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
2.2、结果集封装
因为代码较多,这里不再列出来,可以看以下文章
https://blog.csdn.net/niulinbiao/article/details/120095584
2.3、全局异常处理
有时候不可避免服务器报错的情况,如果不配置异常处理机制,就会默认返回tomcat或者nginx的5XX页面,对普通用户来说,不太友好,用户也不懂什么情况。这时候需要我们程序员设计返回一个友好简单的格式给前端。处理办法如下:通过使用@ControllerAdvice
来进行统一异常处理,@ExceptionHandler(value = RuntimeException.class)
来指定捕获的Exception各个类型异常 ,这个异常的处理,是全局的,所有类似的异常,都会跑到这个地方处理。步骤二、定义全局异常处理,@ControllerAdvice
表示定义全局控制器异常处理,@ExceptionHandler
表示针对性异常处理,可对每种异常针对性处理。
package com.markerhub.common.exception;
import com.markerhub.common.lang.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
//
// 实体校验异常捕获
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result handler(MethodArgumentNotValidException e) {
BindingResult result = e.getBindingResult();
ObjectError objectError = result.getAllErrors().stream().findFirst().get();
log.error("实体校验异常:----------------{}", objectError.getDefaultMessage());
return Result.fail(objectError.getDefaultMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IllegalArgumentException.class)
public Result handler(IllegalArgumentException e) {
log.error("Assert异常:----------------{}", e.getMessage());
return Result.fail(e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = RuntimeException.class)
public Result handler(RuntimeException e) {
log.error("运行时异常:----------------{}", e.getMessage());
return Result.fail(e.getMessage());
}
}
上面我们捕捉了几个异常:
- ShiroException:shiro抛出的异常,比如没有权限,用户登录异常
- IllegalArgumentException:处理Assert的异常
- MethodArgumentNotValidException:处理实体校验的异常
- RuntimeException:捕捉其他异常
2.4、跨域处理
参考文章https://blog.csdn.net/niulinbiao/article/details/120155652
三、整合SpringSecurity
SpringSecruity原理图
我们这里的整合思路图
3.1、引入依赖
springsecurity以及jwt的依赖
<!-- springboot security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--redis缓存验证码-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!--验证码 -->
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>0.0.9</version>
</dependency>
<!-- hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
3.2、Jwt配置
#jwt配置
zzuli:
jwt:
header: Authorization
expire: 604800 #7天 以秒为单位
secret: c1d0f6ba-cd6a-fdf2-b67f-b01d9ef3b554
3.3、工具类
3.3.1、JwtUtils.java
生成以及验证token
package com.zzuli.utils;
import io.jsonwebtoken.*;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 生成token
* 校验token
*/
@Data
@Component
@ConfigurationProperties(prefix = "zzuli.jwt")
public class JwtUtils {
private long expire;
private String secret;
private String header;
// 生成jwt
public String generateToken(String username) {
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + 1000 * expire);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(username)
.setIssuedAt(nowDate)
.setExpiration(expireDate)// 7天過期
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
// 解析jwt
public Claims getClaimByToken(String jwt) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(jwt)
.getBody();
} catch (Exception e) {
return null;
}
}
// jwt是否过期
public boolean isTokenExpired(Claims claims) {
return claims.getExpiration().before(new Date());
}
}
3.3.2、RedisUtil配置
参考文章https://blog.csdn.net/niulinbiao/article/details/120154429
3.4、验证码配置
参考文章https://blog.csdn.net/niulinbiao/article/details/120155012
3.5、登录处理器
3.5.1、登录成功处理器
package com.zzuli.security;
import cn.hutool.core.map.MapUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zzuli.common.api.CommonResult;
import com.zzuli.entity.SysUser;
import com.zzuli.service.SysUserService;
import com.zzuli.utils.JwtUtils;
import com.zzuli.vo.UserVO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
SysUserService userService;
@Autowired
JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
// 生成jwt,并放置到请求头中
String jwt = jwtUtils.generateToken(authentication.getName());
response.setHeader(jwtUtils.getHeader(), jwt);
//封装用户信息
SysUser user = userService.getOne(new QueryWrapper<SysUser>().eq("username", authentication.getName()));
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user,userVO);
userVO.setToken(jwt);
CommonResult<UserVO> result = CommonResult.success(userVO);
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
3.5.2、登陆失败处理器
package com.zzuli.security;
import cn.hutool.json.JSONUtil;
import com.zzuli.common.api.CommonResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
CommonResult<String> result = CommonResult.failed("账号或者密码错误!");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
3.6、自定义验证码过滤器
package com.zzuli.security;
import com.zzuli.common.Const;
import com.zzuli.common.exception.CaptchaException;
import com.zzuli.utils.RedisUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 验证验证么的过滤器
*/
@Component
public class CaptchaFilter extends OncePerRequestFilter {
@Autowired
RedisUtil redisUtil;
@Autowired
LoginFailureHandler loginFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String url = httpServletRequest.getRequestURI();
if ("/login".equals(url) && httpServletRequest.getMethod().equals("POST")) {
try{
// 校验验证码
validate(httpServletRequest);
} catch (CaptchaException e) {
// 交给认证失败处理器
loginFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
// 校验验证码逻辑
private void validate(HttpServletRequest httpServletRequest) {
String code = httpServletRequest.getParameter("code");
String key = httpServletRequest.getParameter("key");
if (StringUtils.isBlank(code) || StringUtils.isBlank(key)) {
throw new CaptchaException("验证码错误");
}
if (!code.equals(redisUtil.hget(Const.CAPTCHA_KEY, key))) {
throw new CaptchaException("验证码错误");
}
// 一次性使用
//清除Redis中的验证码缓存
redisUtil.hdel(Const.CAPTCHA_KEY, key);
}
}
3.7、自定义JWT认证过滤器
package com.zzuli.security;
import cn.hutool.core.util.StrUtil;
import com.zzuli.entity.SysUser;
import com.zzuli.service.SysUserService;
import com.zzuli.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 进行登录的过滤器
* 对token进行校验
*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
JwtUtils jwtUtils;
@Autowired
UserDetailServiceImpl userDetailService;
@Autowired
SysUserService sysUserService;
//在securityConfig中可以通过构造器来注入
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String jwt = request.getHeader(jwtUtils.getHeader());
if (StrUtil.isBlankOrUndefined(jwt)) {
chain.doFilter(request, response);
return;
}
Claims claim = jwtUtils.getClaimByToken(jwt);
if (claim == null) {
throw new JwtException("token 异常");
}
if (jwtUtils.isTokenExpired(claim)) {
throw new JwtException("token已过期");
}
String username = claim.getSubject();
// 获取用户的权限等信息
//完成自动认证
SysUser sysUser = sysUserService.getByUsername(username);
UsernamePasswordAuthenticationToken token
= new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(sysUser.getId()));
SecurityContextHolder.getContext().setAuthentication(token);
chain.doFilter(request, response);
}
}
3.8、异常处理器
3.8.1、用户认证失败处理器
package com.zzuli.security;
import cn.hutool.json.JSONUtil;
import com.zzuli.common.api.CommonResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
*处理认证失败的异常
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ServletOutputStream outputStream = response.getOutputStream();
CommonResult<String> result = CommonResult.failed("请先登录!");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
3.8.2、权限不足处理器
package com.zzuli.security;
import cn.hutool.json.JSONUtil;
import com.zzuli.common.api.CommonResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 处理没有权限的异常
* 当用户没有访问资源的权限时
* 抛出该异常
*/
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
ServletOutputStream outputStream = response.getOutputStream();
CommonResult<String> result = CommonResult.failed(accessDeniedException.getMessage());
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
3.9、从数据库中查询用户
3.9.1、UserDetailServiceImpl .java
package com.zzuli.security;
import com.zzuli.entity.SysUser;
import com.zzuli.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 从数据库中验证用户信息
*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getByUsername(username);
if (sysUser == null) {
throw new UsernameNotFoundException("用户名或密码不正确");
}
//这里包含了密码,就是用于校验密码的
return new AccountUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(), getUserAuthority(sysUser.getId()));
}
/**
* 获取用户权限信息(角色、菜单权限)
* @param userId
* @return
*/
public List<GrantedAuthority> getUserAuthority(Long userId){
// 角色(ROLE_admin)、菜单操作权限 sys:user:list
String authority = sysUserService.getUserAuthorityInfo(userId); // ROLE_admin,ROLE_normal,sys:user:list,....
return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
}
}
3.9.2、AccountUser.java
package com.zzuli.security;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.Assert;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
public class AccountUser implements UserDetails {
private Long userId;
private String password;
private final String username;
private final Collection<? extends GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
public AccountUser(Long userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(userId, username, password, true, true, true, true, authorities);
}
public AccountUser(Long userId, String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null,
"Cannot pass null or empty values to constructor");
this.userId = userId;
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
3.10、给用户增加权限
3.10.1、SysUserService.java
package com.zzuli.service;
import com.zzuli.entity.SysUser;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 服务类
* </p>
*
* @author 关注公众号:One图云
* @since 2021-09-07
*/
public interface SysUserService extends IService<SysUser> {
SysUser getByUsername(String username);
String getUserAuthorityInfo(Long userId);
void clearUserAuthorityInfo(String username);
void clearUserAuthorityInfoByRoleId(Long roleId);
void clearUserAuthorityInfoByMenuId(Long menuId);
}
3.10.2、SysUserServiceImpl.java
package com.zzuli.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zzuli.entity.SysMenu;
import com.zzuli.entity.SysRole;
import com.zzuli.entity.SysUser;
import com.zzuli.mapper.SysUserMapper;
import com.zzuli.service.SysMenuService;
import com.zzuli.service.SysRoleService;
import com.zzuli.service.SysUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zzuli.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* <p>
* 服务实现类
* </p>
*
* @author 关注公众号:One图云
* @since 2021-09-07
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
@Autowired
SysRoleService sysRoleService;
@Autowired
SysUserMapper sysUserMapper;
@Autowired
SysMenuService sysMenuService;
@Autowired
RedisUtil redisUtil;
@Override
public SysUser getByUsername(String username) {
return getOne(new QueryWrapper<SysUser>().eq("username",username));
}
@Override
public String getUserAuthorityInfo(Long userId) {
SysUser sysUser = sysUserMapper.selectById(userId);
// ROLE_admin,ROLE_normal,sys:user:list,....
String authority = "";
if (redisUtil.hasKey("GrantedAuthority:" + sysUser.getUsername())) {
authority = (String) redisUtil.get("GrantedAuthority:" + sysUser.getUsername());
} else {
// 获取角色编码
List<SysRole> roles = sysRoleService.list(new QueryWrapper<SysRole>()
.inSql("id", "select role_id from sys_user_role where user_id = " + userId));
if (roles.size() > 0) {
String roleCodes = roles.stream().map(r -> "ROLE_" + r.getCode()).collect(Collectors.joining(","));
authority = roleCodes.concat(",");
}
// 获取菜单操作编码
List<Long> menuIds = sysUserMapper.getNavMenuIds(userId);
if (menuIds.size() > 0) {
List<SysMenu> menus = (List<SysMenu>) sysMenuService.listByIds(menuIds);
String menuPerms = menus.stream().map(m -> m.getPerms()).collect(Collectors.joining(","));
authority = authority.concat(menuPerms);
}
//将权限信息存入redis,避免频繁查数据库
redisUtil.set("GrantedAuthority:" + sysUser.getUsername(), authority, 60 * 60);
}
return authority;
}
/**
* 清除权限的缓存
* @param username
*/
@Override
public void clearUserAuthorityInfo(String username) {
redisUtil.del("GrantedAuthority:" + username);
}
/**
* 当角色信息发生改变
* 清除缓存
* @param roleId
*/
@Override
public void clearUserAuthorityInfoByRoleId(Long roleId) {
List<SysUser> sysUsers = this.list(new QueryWrapper<SysUser>()
.inSql("id", "select user_id from sys_user_role where role_id = " + roleId));
sysUsers.forEach(u -> {
this.clearUserAuthorityInfo(u.getUsername());
});
}
/**
* 当权限信息发生改变
* 清除缓存
* @param menuId
*/
@Override
public void clearUserAuthorityInfoByMenuId(Long menuId) {
List<SysUser> sysUsers = sysUserMapper.listByMenuId(menuId);
sysUsers.forEach(u -> {
this.clearUserAuthorityInfo(u.getUsername());
});
}
}
3.10.3、SysUserMapper.java
package com.zzuli.mapper;
import com.zzuli.entity.SysUser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* <p>
* Mapper 接口
* </p>
*
* @author 关注公众号:One图云
* @since 2021-09-07
*/
@Repository
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
List<Long> getNavMenuIds(Long userId);
List<SysUser> listByMenuId(Long menuId);
}
3.10.4、SysUserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzuli.mapper.SysUserMapper">
<select id="getNavMenuIds" resultType="java.lang.Long">
SELECT
DISTINCT rm.menu_id
FROM
sys_user_role ur
LEFT JOIN sys_role_menu rm ON ur.role_id = rm.role_id
WHERE ur.user_id = #{userId}
</select>
<select id="listByMenuId" resultType="com.zzuli.entity.SysUser">
SELECT DISTINCT
su.*
FROM
sys_user_role ur
LEFT JOIN sys_role_menu rm ON ur.role_id = rm.role_id
LEFT JOIN sys_user su ON ur.user_id = su.id
WHERE
rm.menu_id = #{menuId}
</select>
</mapper>
3.11、用户退出
package com.zzuli.security;
import cn.hutool.json.JSONUtil;
import com.zzuli.common.api.CommonResult;
import com.zzuli.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 用户退出
*/
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
JwtUtils jwtUtils;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//退出
if (authentication != null) {
new SecurityContextLogoutHandler().logout(request, response, authentication);
}
//返回信息
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
//清除token
response.setHeader(jwtUtils.getHeader(), "");
CommonResult<String> result = CommonResult.success("");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
四、测试
4.1、登录测试
因为登录需要验证码,所以为了方便后台将验证码写死,用postman工具测试时需要增加一个前置请求
pm.sendRequest("http://localhost:8088/api/captcha", function (err, response) {
console.log(response.json());
});
登陆成功
4.2、权限测试
后端随便写两个方法,一个需要角色,另一个需要权限
package com.zzuli.controller;
import com.zzuli.common.api.CommonResult;
import com.zzuli.entity.SysUser;
import com.zzuli.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
/**
* @author niuben
*/
@RestController
public class TestController {
@Autowired
private SysUserService sysUserService;
@Autowired
private BCryptPasswordEncoder cryptPasswordEncoder;
@PreAuthorize("hasRole('admin')") //必须由角色
@GetMapping("/test")
public CommonResult<List<SysUser>> test(){
List<SysUser> sysUserList = sysUserService.list();
return CommonResult.success(sysUserList);
}
@PreAuthorize("hasAuthority('sys:user:list')") //必须有权限
@GetMapping("/addpass")
public CommonResult<Object> pass(){
//加密后的密码
String encode = cryptPasswordEncoder.encode("123456");
boolean matches = cryptPasswordEncoder.matches("123456", encode);
System.out.println("匹配结果:"+matches);
return CommonResult.success(encode);
}
}
没有权限的用户登录,数据库中的test用户
拥有权限的用户登录,数据库中的admin用户