Spring Security+JWT实现认证与授权的实现

网友投稿 275 2022-08-02


目录一、登录校验流程1、Spring Security 完整流程2、Spring Security的默认登陆验证流程。3、 整合JWT大致流程前端响应类JWT工具类重写UserDetailsService的方法重写登录接口认证过滤器退出登陆授权基本流程限制访问资源所需权限封装权限信息RBAC权限模型自定义失败处理

认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

授权:经过认证后判断当前用户是否有权限进行某个操作

一、登录校验流程

1、Spring Security 完整流程

SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。部分核心过滤器如下图:

UsernamePasswordAuthenticationFilter:负责处理在登录页填写了用户名密码后的登陆请求。

ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException(访问出错)和AuthenticationExcption(认证出错)。

FilterSecurityInterceptor:负责权限校验的过滤器。

2、Spring Security的默认登陆验证流程。

Authentication接口:它的实现类,表示当前访问系统的用户,封装了用户相关信息。

AuthenticationManager接口:定义了认证Authentication的方法。

UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。

UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

3、 整合JWT大致流程

登录

①自定义登录接口

调用ProviderManager的方法进行认证 如果认证通过生成JWT。

把用户信息存入redis中

②自定义UserDetailsService

在这个实现类中去查询数据库

校验

①定义Jwt认证过滤器

获取token

解析token获取其中的userid

从redis中获取用户信息

存入SecurityContextHolder

Redis使用Fastjson序列化

org.springframework.boot

spring-boot-starter-data-redis

org.apache.commons

commons-pool2

com.alibaba

fastjson

1.2.76

import com.alibaba.fastjson.JSON;

import com.alibaba.fastjson.parser.ParserConfig;

import com.alibaba.fastjson.serializer.SerializerFeature;

import org.springframework.data.redis.serializer.RedisSerializer;

import org.springframework.data.redis.serializer.SerializationException;

import java.nio.charset.Charset;

import java.nio.charset.StandardCharsets;

public class FastJson2JsonRedisSerializer implements RedisSerializer {

public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

static {

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

}

private final Class clazz;

public FastJson2JsonRedisSerializer(Class clazz) {

super();

this.clazz = clazz;

}

/**

* 序列化

*/

@Override

public byte[] serialize(T t) throws SerializationException {

if (null == t) {

return new byte[0];

}

return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);

}

/**

* 反序列化

*/

@Override

public T deserialize(byte[] bytes) throws SerializationException {

if (null == bytes || bytes.length <= 0) {

return null;

}

String str = new String(bytes, DEFAULT_CHARSET);

return (T) JSON.parseObject(str, clazz);

}

}

import org.springframework.boot.autoconfigure.AutoConfigureAfter;

import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.redis.connection.RedisConnectionFactory;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration

@AutoConfigureAfter(RedisAutoConfiguration.class)

public class RedisCacheAutoConfiguration {

@Bean

public RedisTemplate redisTemplate(RedisConnectionFactory factory) {

RedisTemplate template = new RedisTemplate<>();

template.setConnectionFactory(factory);

FastJson2JsonRedisSerializer fastJsonRedisSerializer = new FastJson2JsonRedisSerializer<>(Object.class);

StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

// key采用String的序列化方式

template.setKeySerializer(stringRedisSerializer);

// hash的key也采用String的序列化方式

template.setHashKeySerializer(stringRedisSerializer);

// value序列化方式采用fastJson

template.setValueSerializer(fastJsonRedisSerializer);

// hash的value序列化方式采用fastJson

template.setHashValueSerializer(fastJsonRedisSerializer);

template.afterPropertiesSet();

return template;

}

}

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.*;

import org.springframework.stereotype.Component;

import java.util.*;

import java.util.concurrent.TimeUnit;

/**

* spring redis 工具类

**/

@Component

public class RedisUtil {

@Autowired

private RedisTemplate redisTemplate;

/**

* 缓存基本的对象,Integer、String、实体类等

*

* @param key 缓存的键值

* @param value 缓存的值

* @return 缓存的对象

*/

public ValueOperations setCacheObject(Object key, Object value) {

ValueOperations operation = redisTemplate.opsForValue();

operation.set(key, value);

return operation;

}

/**

* 缓存基本的对象,Integer、String、实体类等

*

* @param key 缓存的键值

* @param value 缓存的值

* @param timeout 时间

* @param timeUnit 时间颗粒度

* @return 缓存的对象

*/

public ValueOperations setCacheObject(Object key, Object value, Integer timeout, TimeUnit timeUnit) {

ValueOperations operation = redisTemplate.opsForValue();

operation.set(key, value, timeout, timeUnit);

return operation;

}

/**

* 获得缓存的基本对象。

*

* @param key 缓存键值

* @return 缓存键值对应的数据

*/

public Object getCacheObject(Object key) {

ValueOperations operation = redisTemplate.opsForValue();

return operation.get(key);

}

/**

* 删除单个对象

*

* @param key

*/

public void deleteObject(Object key) {

redisTemplate.delete(key);

}

/**

* 删除集合对象

*

* @param collection

*/

public void deleteObject(Collection collection) {

redisTemplate.delete(collection);

}

public Long getExpire(String key) {

return redisTemplate.getExpire(key);

}

public void expire(String key, int expire, TimeUnit timeUnit) {

redisTemplate.expire(key, expire, timeUnit);

}

/**

* 缓存List数据

*

* @param key 缓存的键值

* @param dataList 待缓存的List数据

* @return 缓存的对象

*/

public ListOperations setCacheList(Object key, List dataList) {

ListOperations listOperation = redisTemplate.opsForList();

if (null != dataList) {

int size = dataList.size();

for (Object o : dataList) {

listOperation.leftPush(key, o);

}

}

return listOperation;

}

/**

* 获得缓存的list对象

*

* @param key 缓存的键值

* @return 缓存键值对应的数据

*/

public List getCacheList(String key) {

List dataList = new ArrayList<>();

ListOperations listOperation = redisTemplate.opsForList();

Long size = listOperation.size(key);

if (null != size) {

for (int i = 0; i < size; i++) {

dataList.add(listOperation.index(key, i));

}

}

return dataList;

}

/**

* 缓存Set

*

* @param key 缓存键值

* @param dataSet 缓存的数据

* @return 缓存数据的对象

*/

public BoundSetOperations setCacheSet(String key, Set dataSet) {

BoundSetOperations setOperation = redisTemplate.boundSetOps(key);

for (Object o : dataSet) {

setOperation.add(o);

}

return setOperation;

}

/**

* 获得缓存的set

*

* @param key

* @return

*/

public Set getCacheSet(Object key) {

Set dataSet = new HashSet<>();

BoundSetOperations operation = redisTemplate.boundSetOps(key);

dataSet = operation.members();

return dataSet;

}

/**

* 缓存Map

*

* @param key

* @param dataMap

* @return

*/

public HashOperations setCacheMap(Object key, Map dataMap) {

HashOperations hashOperations = redisTemplate.opsForHash();

if (null != dataMap) {

for (Map.Entry entry : dataMap.entrySet()) {

hashOperations.put(key, entry.getKey(), entry.getValue());

}

}

return hashOperations;

}

/**

* 获得缓存的Map

*

* @param key

* @return

*/

public Map getCacheMap(Object key) {

Map map = redisTemplate.opsForHash().entries(key);

return map;

}

/**

* 获得缓存的基本对象列表

*

* @param pattern 字符串前缀

* @return 对象列表

*/

public Collection keys(String pattern) {

return redisTemplate.keys(pattern);

}

}

前端响应类

@JsonInclude(JsonInclude.Include.NON_NULL)

public class ResponseResult {

/**

* 状态码

*/

private Integer code;

/**

* 提示信息,如果有错误时,前端可以获取该字段进行提示

*/

private String msg;

/**

* 查询到的结果数据,

*/

private T data;

public ResponseResult(Integer code, String msg) {

this.code = code;

this.msg = msg;

}

public ResponseResult(Integer code, T data) {

this.code = code;

this.data = data;

}

public Integer getCode() {

return code;

}

public void setCode(Integer code) {

this.code = code;

}

public String getMsg() {

return msg;

}

public void setMsg(String msg) {

this.msg = msg;

}

public T getData() {

return data;

}

public void setData(T data) {

this.data = data;

}

public ResponseResult(Integer code, String msg, T data) {

this.code = code;

this.msg = msg;

this.data = data;

}

JWT工具类

public class JwtUtil {

//有效期为

public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时

//设置秘钥明文

public static final String JWT_KEY = "zhangao";

public static String getUUID(){

String token = UUID.randomUUID().toString().replaceAll("-", "");

return token;

}

/**

* 生成jtw

* @param subject token中要存放的数据(json格式)

* @return

*/

public static String createJWT(String subject) {

JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间

return builder.compact();

}

/**

* 生成jtw

* @param subject token中要存放的数据(json格式)

* @param ttlMillis token超时时间

* @return

*/

public static String createJWT(String subject, Long ttlMillis) {

JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间

return builder.compact();

}

private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {

SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

SecretKey secretKey = generalKey();

long nowMillis = System.currentTimeMillis();

Date now = new Date(nowMillis);

if(ttlMillis==null){

ttlMillis=JwtUtil.JWT_TTL;

}

long expMillis = nowMillis + ttlMillis;

Date expDate = new Date(expMillis);

return Jwts.builder()

.setId(uuid) //唯一的ID

.setSubject(subject) // 主题 可以是JSON数据

.setIssuer("sg") // 签发者

.setIssuedAt(now) // 签发时间

.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥

.setExpiration(expDate);

}

/**

* 创建token

* @param id

* @param subject

* @param ttlMillis

* @return

*/

public static String createJWT(String id, String subject, Long ttlMillis) {

JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间

return builder.compact();

}

public static void main(String[] args) throws Exception {

// String jwt = createJWT("2123");

Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyOTY2ZGE3NGYyZGM0ZDAxOGU1OWYwNjBkYmZkMjZhMSIsInN1YiI6IjIiLCJpc3MiOiJzZyIsImlhdCI6MTYzOTk2MjU1MCwiZXhwIjoxNjM5OTY2MTUwfQ.NluqZnyJ0gHz-2wBIari2r3XpPp06UMn4JS2sWHILs0");

String subject = claims.getSubject();

System.out.println(subject);

// System.out.println(claims);

}

/**

* 生成加密后的秘钥 secretKey

* @return

*/

public static SecretKey generalKey() {

byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);

SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");

return key;

}

/**

* 解析

*

* @param jwt

* @return

* @throws Exception

*/

public static Claims parseJWT(String jwt) throws Exception {

SecretKey secretKey = generalKey();

return Jwts.parser()

.setSigningKey(secretKey)

.parseClaimsJws(jwt)

.getBody();

}

}

创建数据库表信息和实体,配置数据库连接信息

定义mapper等一系列接口。xml等。用mybatis-plus方便一点,注意Mapper继承BaseMapper<实体类>,实体类中需要加@TableName(value = "表名") ,id字段上加 @TableId

在application.yml中配置mapperXML文件的位置

引入依赖

com.baomidou

mybatis-plus-boot-starter

3.4.3

mysql

mysql-connector-java

重写UserDetailsService的方法

创建一个类实现UserDetailsService接口,重写其中的方法。从数据库中查询用户信息,进行校验。(如果没有重写的话,就是上面说的spring security默认的使用UserDetailsService接口下面的InMemoryUserDetailsManager实现类中的方法,是在内存中查找。这个是需要根据我们具体的系统来重写的。)

@Service

public class UserDetailsServiceImpl implements UserDetailsService {

@Autowired

private UserMapper userMapper;

@Autowired

private MenuMapper menuMapper;

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

//查询用户信息

LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();

queryWrapper.eq(User::getUserName,username);

User user = userMapper.selectOne(queryWrapper);

//如果没有查询到用户就抛出异常

if(Objects.isNull(user)){

throw new RuntimeException("用户名或者密码错误");

}

// 查询权限

List list = menuMapper.selectPermsByUserId(user.getId());

//把数据封装成UserDetails返回

return new LoginUser(user,list);

}

}

因为UserDetailsService方法的返回值是UserDetails类型,所以需要定义一个类,实现该接口,把用户信息封装在其中。

@Data

@NoArgsConstructor

public class LoginUser implements UserDetails {

private User user;

private List permissions;

public LoginUser(User user, List permissions) {

this.user = user;

this.permissions = permissions;

}

@JSONField(serialize = false)

private List authorities;

@Override

public Collection extends GrantedAuthority> getAuthorities() {

if(authorities!=null){

return authorities;

}

//把permissions中String类型的权限信息封装成SimpleGrantedAuthority对象

// authorities = new ArrayList<>();

// for (String permission : permissions) {

// SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);

// authorities.add(authority);

// }

authorities = permissions.stream()

.map(SimpleGrantedAuthority::new)

.collect(Collectors.toList());

return authorities;

}

@Override

public String getPassword() {

return user.getPassword();

}

@Override

public String getUsername() {

return user.getUserName();

}

@Override

public boolean isAccountNonExpired() {

return true;

}

@Override

public boolean isAccountNonLocked() {

return true;

}

@Override

public boolean isCredentialsNonExpired() {

return true;

}

@Override

public boolean isEnabled() {

return true;

}

}

重写登录接口

接下我们需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。

在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。

认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体

的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。

@RestController

public class LoginController {

@Autowired

private LoginServcie loginServcie;

@PostMapping("/user/login")

public ResponseResult login(@RequestBody User user){

//登录

return loginServcie.login(user);

}

@RequestMapping("/user/logout")

public ResponseResult logout(){

return loginServcie.logout();

}

}

@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter { //创建BCryptPasswordEncoder注入容器 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private AccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http //关闭csrf .csrf().disable() //不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口 允许匿名访问 .antMatchers("/user/login").anonymous()// .antMatchers("/testCors").hasAuthority("system:dept:list222") // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); //添加过滤器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //配置异常处理器 http.exceptionHandling() //配置认证失败处理器 .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); //允许跨域 http.cors(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }

@Configuration

@EnableGlobalMethodSecurity(prePostEnabled = true)

public class SecurityConfig extends WebSecurityConfigurerAdapter {

//创建BCryptPasswordEncoder注入容器

@Bean

public PasswordEncoder passwordEncoder(){

return new BCryptPasswordEncoder();

}

@Autowired

private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

@Autowired

private AuthenticationEntryPoint authenticationEntryPoint;

@Autowired

private AccessDeniedHandler accessDeniedHandler;

@Override

protected void configure(HttpSecurity http) throws Exception {

http

//关闭csrf

.csrf().disable()

//不通过Session获取SecurityContext

.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

.and()

.authorizeRequests()

// 对于登录接口 允许匿名访问

.antMatchers("/user/login").anonymous()

// .antMatchers("/testCors").hasAuthority("system:dept:list222")

// 除上面外的所有请求全部需要鉴权认证

.anyRequest().authenticated();

//添加过滤器

http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

//配置异常处理器

http.exceptionHandling()

//配置认证失败处理器

.authenticationEntryPoint(authenticationEntryPoint)

.accessDeniedHandler(accessDeniedHandler);

//允许跨域

http.cors();

}

@Bean

@Override

public AuthenticationManager authenticationManagerBean() throws Exception {

return super.authenticationManagerBean();

}

@Service

public class LoginServiceImpl implements LoginServcie {

@Autowired

private AuthenticationManager authenticationManager;

@Autowired

private RedisCache redisCache;

@Override

public ResponseResult login(User user) {

//AuthenticationManager authenticate进行用户认证

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());

Authentication authenticate = authenticationManager.authenticate(authenticationToken);

//如果认证没通过,给出对应的提示

if(Objects.isNull(authenticate)){

throw new RuntimeException("登录失败");

}

//如果认证通过了,使用userid生成一个jwt jwt存入ResponseResult返回

LoginUser loginUser = (LoginUser) authenticate.getPrincipal();

String userid = loginUser.getUser().getId().toString();

String jwt = JwtUtil.createJWT(userid);

Map map = new HashMap<>();

map.put("token",jwt);

//把完整的用户信息存入redis userid作为key

redisCache.setCacheObject("login:"+userid,loginUser);

return new ResponseResult(200,"登录成功",map);

}

@Override

public ResponseResult logout() {

//获取SecurityContextHolder中的用户id

UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();

LoginUser loginUser = (LoginUser) authentication.getPrincipal();

Long userid = loginUser.getUser().getId();

//删除redis中的值

redisCache.deleteObject("login:"+userid);

return new ResponseResult(200,"注销成功");

}

}

认证过滤器

我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。(把这个放到最前面,放到UsernamePassword的那个前面)这样做就是为了除了登录的时候去查询数据库外,其他时候都用JWT配合Redis进行认证。

使用userid去redis中获取对应的LoginUser对象。

然后封装Authentication对象存入SecurityContextHolder

@Component

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

@Autowired

private RedisCache redisCache;

@Override

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

//获取token

String token = request.getHeader("token");

if (!StringUtils.hasText(token)) {

//放行

filterChain.doFilter(request, response);

return;

}

//解析token

String userid;

try {

Claims claims = JwtUtil.parseJWT(token);

userid = claims.getSubject();

} catch (Exception e) {

e.printStackTrace();

throw new RuntimeException("token非法");

}

//从redis中获取用户信息

String redisKey = "login:" + userid;

LoginUser loginUser = redisCache.getCacheObject(redisKey);

if(Objects.isNull(loginUser)){

throw new RuntimeException("用户未登录");

}

//存入SecurityContextHolder

//TODO 获取权限信息封装到Authentication中

UsernamePasswordAuthenticationToken authenticationToken =

new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());

SecurityContextHolder.getContext().setAuthentication(authenticationToken);

//放行

filterChain.doFilter(request, response);

}

}

退出登陆

我们只需要定义一个登陆接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可。

@Override

public ResponseResult logout() {

//获取SecurityContextHolder中的用户id

UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();

LoginUser loginUser = (LoginUser) authentication.getPrincipal();

Long userid = loginUser.getUser().getId();

//删除redis中的值

redisCache.deleteObject("login:"+userid);

return new ResponseResult(200,"注销成功");

}

授权基本流程

在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。

所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。

然后设置我们的资源所需要的权限即可。

限制访问资源所需权限

SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。

但是要使用它我们需要先开启相关配置。

然后就可以使用对应的注解。@PreAuthorize

@RestController

public class HelloController {

@RequestMapping("/hello")

@PreAuthorize("hasAuthority('test')")

public String hello(){

return "hello";

}

}

封装权限信息

我们前面在写UserDetailsServiceImpl的时候说过,在查询出用户后还要获取对应的权限信息,封装到UserDetails中返回。

@Service

public class UserDetailsServiceImpl implements UserDetailsService {

@Autowired

private UserMapper userMapper;

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();

wrapper.eq(User::getUserName,username);

User user = userMapper.selectOne(wrapper);

if(Objects.isNull(user)){

throw new RuntimeException("用户名或密码错误");

}

//TODO 根据用户查询权限信息 添加到LoginUser中

List list = new ArrayList<>(Arrays.asList("test"));

return new LoginUser(user,list);

}

}

RBAC权限模型

RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。

参考表:

CREATE DATABASE /*!32312 IF NOT EXISTS*/`sg_security` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;

USE `sg_security`;

/*Table structure for table `sys_menu` */

DROP TABLE IF EXISTS `sys_menu`;

CREATE TABLE `sys_menu` (

`id` bigint(20) NOT NULL AUTO_INCREMENT,

`menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',

`path` varchar(200) DEFAULT NULL COMMENT '路由地址',

`component` varchar(255) DEFAULT NULL COMMENT '组件路径',

`visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',

`status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',

`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',

`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',

`create_by` bigint(20) DEFAULT NULL,

`create_time` datetime DEFAULT NULL,

`update_by` bigint(20) DEFAULT NULL,

`update_time` datetime DEFAULT NULL,

`del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',

`remark` varchar(500) DEFAULT NULL COMMENT '备注',

PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';

/*Table structure for table `sys_role` */

DROP TABLE IF EXISTS `sys_role`;

CREATE TABLE `sys_role` (

`id` bigint(20) NOT NULL AUTO_INCREMENT,

`name` varchar(128) DEFAULT NULL,

`role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',

`status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',

`del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',

`create_by` bigint(200) DEFAULT NULL,

`create_time` datetime DEFAULT NULL,

`update_by` bigint(200) DEFAULT NULL,

`update_time` datetime DEFAULT NULL,

`remark` varchar(500) DEFAULT NULL COMMENT '备注',

PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

/*Table structure for table `sys_role_menu` */

DROP TABLE IF EXISTS `sys_role_menu`;

CREATE TABLE `sys_role_menu` (

`role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',

`menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id',

PRIMARY KEY (`role_id`,`menu_id`)

) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

/*Table structure for table `sys_user` */

DROP TABLE IF EXISTS `sys_user`;

CREATE TABLE `sys_user` (

`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',

`user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',

`nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',

`password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',

`status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',

`email` varchar(64) DEFAULT NULL COMMENT '邮箱',

`phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',

`sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',

`avatar` varchar(128) DEFAULT NULL COMMENT '头像',

`user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',

`create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id',

`create_time` datetime DEFAULT NULL COMMENT '创建时间',

`update_by` bigint(20) DEFAULT NULL COMMENT '更新人',

`update_time` datetime DEFAULT NULL COMMENT '更新时间',

`del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',

PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

/*Table structure for table `sys_user_role` */

DROP TABLE IF EXISTS `sys_user_role`;

CREATE TABLE `sys_user_role` (

`user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',

`role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',

PRIMARY KEY (`user_id`,`role_id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

查询条件

SELECT

DISTINCT m.`perms`

FROM

sys_user_role ur

LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`

LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`

LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`

WHERE

user_id = 2

AND r.`status` = 0

AND m.`status` = 0

/**

* 菜单表(Menu)实体类

*

* @author makejava

* @since 2021-11-24 15:30:08

*/

@TableName(value="sys_menu")

@Data

@AllArgsConstructor

@NoArgsConstructor

@JsonInclude(JsonInclude.Include.NON_NULL)

public class Menu implements Serializable {

private static final long serialVersionUID = -54979041104113736L;

@TableId

private Long id;

/**

* 菜单名

*/

private String menuName;

/**

* 路由地址

*/

private String path;

/**

* 组件路径

*/

private String component;

/**

* 菜单状态(0显示 1隐藏)

*/

private String visible;

/**

* 菜单状态(0正常 1停用)

*/

private String status;

/**

* 权限标识

*/

private String perms;

/**

* 菜单图标

*/

private String icon;

private Long createBy;

private Date createTime;

private Long updateBy;

private Date updateTime;

/**

* 是否删除(0未删除 1已删除)

*/

private Integer delFlag;

/**

* 备注

*/

private String remark;

}

public interface MenuMapper extends BaseMapper

List selectPermsByUserId(Long id);

}

SELECT

DISTINCT m.`perms`

FROM

sys_user_role ur

LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`

LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`

LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`

WHERE

user_id = #{userid}

AND r.`status` = 0

AND m.`status` = 0

mybatis-plus:

mapper-locations: classpath*:/mapper/**/*.xml

自定义失败处理

我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。

在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。

如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。

所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。

@Component

public class AccessDeniedHandlerImpl implements AccessDeniedHandler {

@Override

public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {

ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "权限不足");

String json = JSON.toJSONString(result);

WebUtils.renderString(response,json);

}

}

@Component

public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {

@Override

public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");

String json = JSON.toJSONString(result);

WebUtils.renderString(response,json);

}

}

public class WebUtils

{

/**

* 将字符串渲染到客户端

*

* @param response 渲染对象

* @param string 待渲染的字符串

* @return null

*/

public static String renderString(HttpServletResponse response, String string) {

try

{

response.setStatus(200);

response.setContentType("application/json");

response.setCharacterEncoding("utf-8");

response.getWriter().print(string);

}

catch (IOException e)

{

e.printStackTrace();

}

return null;

}

}

配置给SpringSecurity


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Java详细分析Lambda表达式与Stream流的使用方法
下一篇:SpringCloud高可用配置中心Config详解(springcloud配置中心consul)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~