SpringBoot日常:集成security认证授权

接入数据源

配置redis

1、pom依赖包
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.7.13</version>
        </dependency>
		<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
2、redis配置类

RedisConfig

import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
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
public class RedisConfig {

    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}
3、redis工具类

RedisCache


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache {

    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
    {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 删除Hash中的数据
     *
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey)
    {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }

}

4、application.yml配置

添加redis的相关连接配置


spring:
  datasource:
    redis:
      host: 127.0.0.1
      port: 6379
      database: 0
      timeout: 10000ms
      lettuce:
        pool:
          max-active: 8
          max-wait: 10000ms
          max-idle: 8
          min-idle: 5

配置mysql

1、pom依赖包

引入MybatisPuls、mysql驱动

		<dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3.4</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
2、application.yml配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/share?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
3、创建数据库
CREATE TABLE `s_sys_user` (
  	`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  	`account` varchar(64) NOT NULL DEFAULT '' COMMENT '账号',
  	`password` varchar(64) NOT NULL DEFAULT '' COMMENT '密码',
    `mobile` varchar(64) NOT NULL DEFAULT '' COMMENT '电话',
    `nick_name` varchar(64) NOT NULL DEFAULT '' COMMENT '昵称',
  	`status` tinyint(1)  NOT NULL DEFAULT  1 COMMENT '状态',
 	`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  	`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  	`create_by` varchar(32) NOT NULL DEFAULT '' COMMENT '创建人',
  	`update_by` varchar(32) NOT NULL DEFAULT '' COMMENT '修改人',
  	`remark_` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
  	`isvalid` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否有效 0否,1是',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'
4、创建User实体类
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;

@Data
@TableName(value = "s_sys_user")
public class SysUser implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @TableField(value = "account")
    private String account;

    @TableField(value = "password")
    private String password;

    @TableField(value = "mobile")
    private String mobile;

    @TableField(value = "nick_name")
    private String nickName;

    @TableField(value = "nick_name")
    private Integer status;
}

5、创建Mapper接口文件
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cys.share.user.entiy.SysUser;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<SysUser> {
}

6、配置Mapper扫描

在启动类添加注解@MapperScan(“com.cys.share.user.mapper”)

@SpringBootApplication
@MapperScan("com.cys.share.user.mapper")
public class UserApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }

}

集成security

1、认证流程实现

步骤一、添加pom依赖包
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>2.7.13</version>
            <exclusions><!-- 去掉springboot默认配置 -->
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
		<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        
		<dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.14.1</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.14.1</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.14.1</version>
        </dependency>
步骤二、新增登录授权类LoginUser
import com.cys.share.user.entiy.SysUser;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    private SysUser sysUser;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return sysUser.getPassword();
    }

    @Override
    public String getUsername() {
        return sysUser.getAccount();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
步骤三、新增实现类UserDetailsServiceImpl
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cys.share.user.dto.LoginUser;
import com.cys.share.user.entiy.SysUser;
import com.cys.share.user.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
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.Objects;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户信息
        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SysUser::getAccount, username);
        SysUser user = userMapper.selectOne(queryWrapper);
        // 如果没有查询到用户则抛出异常,在过滤链中有异常捕获,这里抛出的异常会被捕获
        if (Objects.isNull(user)) {
            throw new RuntimeException("用户名或密码错误");
        }

        //TODO 查询对应的权限信息

        // 把数据封装成UserDetails返回
        return new LoginUser(user);
    }
}
步骤四、新增配置类
import com.cys.share.user.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许访问
                .antMatchers("/login").permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}
步骤五、创建登录接口

这里创建两个接口,一个登录接口login,一个常规接口test。test接口主要是用做后续进行测试

@RestController
public class UserAuthController {

    @Autowired
    private UserAuthService userAuthService;

    @PostMapping("/login")
    public Result login(@RequestBody UserAuthDto userAuthDto) {
        return userAuthService.login(userAuthDto);
    }
	
	@PostMapping("/test")
    public Result test() {
    	return Result.success("test成功");
    }
}
步骤六、登录功能的实现
import com.cys.share.user.dto.LoginUser;
import com.cys.share.user.dto.UserAuthDto;
import com.cys.share.user.service.UserAuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@Service
public class UserAuthServiceImpl implements UserAuthService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    @Override
    public Result login(UserAuthDto user) {
        // 认证的时候需要Authentication对象,所以需Au要一个Authentication的实现类,这里选择了UsernamePasswordAuthenticationToken
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(user.getAccount(),user.getPassword());

        // AuthenticationManager authenticate方法进行认证。在SecurityConfig配置类中,我们将AuthenticationManager注入到容器中。
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

        // 如果认证通过,authenticate里将包含principal属性,该属性的值就是LoginUser,
        // 如果认证没通过,给出对应的提示
        if (Objects.isNull(authenticate)) {
            throw new RuntimeException("登录失败");
        }

        // 如果认证通过了,使用userid生成一个jwt jwt存入ResponseResult返回
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String id = loginUser.getSysUser().getId().toString();
        String jwt = JwtUtil.createJWT(id);
        Map<String, String> map = new HashMap<>();
        map.put("token", jwt);

        // 把完整的用户信息存入redis,userid作为key
        redisCache.setCacheObject("login:" + id, loginUser);

        return Result.success(map );
    }
}
步骤七、新增jwt认证过滤器
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
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;
import java.util.Objects;

@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 id;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            id = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }

        // 从redis中获取用户信息
        String redisKey = "login:" + id;
        Object cacheObject = redisCache.getCacheObject(redisKey);
        LoginUser loginUser = JSON.parseObject(JSON.toJSONString(cacheObject),LoginUser.class);
        if (Objects.isNull(loginUser)) {
            throw new RuntimeException("用户未登录");
        }

        // 后面将需要一个Authentication的对象,在这里通过实现类UsernamePasswordAuthenticationToken构造这个对象
        // 选择3个参数的构造器,principal:账号,credentials:密码,authorities:权限
        // 为什么要选择这个构造器呢?因为这个构造器中有,super.setAuthenticated(true); 标识用户为已认证。
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
        // 存入SecurityContextHolder
        // 存入需要一个Authentication的对象,在登录的时候也用到过类似的方法。
        // TODO 获取权限信息封装到Authentication
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        // 放行
        filterChain.doFilter(request, response);

    }
}
步骤八、SecurityConfig添加过滤器配置
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;


http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
步骤九、测试登录

实际项目中我们不会把密码明文存储在数据库中,SpringSecurity为我们提供的BCryptPasswordEncoder工具进行加密。现在我们需要做两件事,
第一件:测试BCryptPasswordEncoder的加密功能
第二件:将密码加密后手动存到数据库中,这样我们才能验证登录功能

测试BCryptPasswordEncoder的加密功能

	@Test
    public void test(){
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode("12345678");
        //这里输出:$2a$10$6GpCxT.ZndZQFWmfCVc0gO0d7lEEtDL6fLIEAHRcaU8E787WPdXo6
        System.out.println(encode);
    }

手动将加密后的密码存到数据库
在这里插入图片描述

进行登录测试,登录成功返回token
在这里插入图片描述

进行其他接口测试

前面我们还创建了一个test接口,现在进行调用测试,调用时headers中需要添加登录返回的token,不然会失败,测试结果如下
在这里插入图片描述

步骤十、实现登出接口

controller添加接口:

    @GetMapping("/loginOut")
    public Result loginOut() {
        return userAuthService.logout();
    }

userAuthService添加登出实现:

    @Override
    public Result logout() {
        // 获取SecurityContextHolder中的用户id
        UsernamePasswordAuthenticationToken authentication =
                (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long id = loginUser.getSysUser().getId();

        // 删除redis当中的值
        redisCache.deleteObject("login:" + id);

        return Result.success("","注销成功");
    }

测试退出功能
调用loginOut接口,退出成功。再次调用test接口则失败
在这里插入图片描述

2、授权流程实现

步骤一、创建相关的权限控制表

RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。其中包括五张表,分别是用户表(前面我们已经创建)、菜单权限表、角色表、角色和菜单权限之间的关系以及用户和角色之间的关系表。

CREATE TABLE `s_sys_user` (
   `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
   `account` varchar(64) NOT NULL DEFAULT '' COMMENT '账号',
   `password` varchar(64) NOT NULL DEFAULT '' COMMENT '密码',
    `mobile` varchar(64) NOT NULL DEFAULT '' COMMENT '电话',
    `nick_name` varchar(64) NOT NULL DEFAULT '' COMMENT '昵称',
   `status` tinyint(1)  NOT NULL DEFAULT  1 COMMENT '状态',
   `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
   `create_by` varchar(32) NOT NULL DEFAULT '' COMMENT '创建人',
   `update_by` varchar(32) NOT NULL DEFAULT '' COMMENT '修改人',
   `remark_` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
   `isvalid` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否有效 0否,1是',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'

CREATE TABLE `s_sys_role` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `name` varchar(128) NOT NULL DEFAULT ''  COMMENT '账号',
    `role_key` varchar(512) DEFAULT NULL COMMENT '角色权限字符串',
    `status` tinyint(1) DEFAULT 1 COMMENT '角色状态(1正常 0停用)',
    `create_by` bigint(200) DEFAULT NULL,
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `update_by` bigint(200) DEFAULT NULL,
    `remark_` varchar(500) DEFAULT NULL COMMENT '备注',
    `isvalid` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否有效 0否,1是',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';


CREATE TABLE `s_sys_menu` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `menu_name` varchar(64) NOT NULL DEFAULT '' COMMENT '权限菜单名',
    `path` varchar(255) DEFAULT NULL COMMENT '路由地址',
    `component` varchar(255) DEFAULT NULL COMMENT '组件路径',
    `is_view` tinyint(1) DEFAULT 1 COMMENT '菜单状态(1显示 0隐藏)',
    `status` tinyint(1) DEFAULT 1 COMMENT '菜单状态(1正常 0停用)',
    `perms` varchar(128) DEFAULT NULL COMMENT '权限标识',
    `icon` varchar(128) DEFAULT '#' COMMENT '菜单图标',
    `create_by` bigint(200) DEFAULT NULL,
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `update_by` bigint(200) DEFAULT NULL,
    `remark_` varchar(500) DEFAULT NULL COMMENT '备注',
    `isvalid` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否有效 0否,1是',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='权限菜单表';


CREATE TABLE `s_sys_role_menu` (
     `role_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
     `menu_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '菜单id',
    `create_by` bigint(200) DEFAULT NULL,
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `update_by` bigint(200) DEFAULT NULL,
    `remark_` varchar(500) DEFAULT NULL COMMENT '备注',
    `isvalid` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否有效 0否,1是',
     PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='角色与菜单权限关系表';



CREATE TABLE `s_sys_user_role` (
     `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户id',
     `role_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '角色id',
    `create_by` bigint(200) DEFAULT NULL,
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `update_by` bigint(200) DEFAULT NULL,
    `remark_` varchar(500) DEFAULT NULL COMMENT '备注',
    `isvalid` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否有效 0否,1是',
     PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户与角色关系表';

步骤二、创建相关的项目文件

根据表结构,这里需要创建相应的实体类和mapper文件

实体类


@Data
@TableName(value = "s_sys_menu")
public class SysMenu implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @TableField(value = "menu_name")
    private String menuName;

    @TableField(value = "path")
    private String path;

    @TableField(value = "component")
    private String component;

    @TableField(value = "is_view")
    private Integer isView;

    @TableField(value = "status")
    private Integer status;

    @TableField(value = "perms")
    private String perms;

    @TableField(value = "icon")
    private String icon;
}

@Data
@TableName(value = "s_sys_role")
public class SysRole implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @TableField(value = "name")
    private String name;

    @TableField(value = "role_key")
    private String roleKey;

    @TableField(value = "status")
    private Integer status;

}
@Data
@TableName(value = "s_sys_role_menu")
public class SysRoleMenu implements Serializable {

    private static final long serialVersionUID = 1L;


    @TableField(value = "role_id")
    private Long roleId;

    @TableField(value = "menu_id")
    private Long menuId;


}
@Data
@TableName(value = "s_sys_user_role")
public class SysUserRole implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableField(value = "user_id")
    private Long userId;

    @TableField(value = "role_id")
    private Long roleId;


}

mapper接口文件

@Mapper
public interface MenuMapper extends BaseMapper<SysMenu> {

    /**
     * 根据用户id获取权限
     * @param userId 用户id
     * @return
     */
    List<String> listPermsByUserId(Long userId);

}


@Mapper
public interface RoleMapper extends BaseMapper<SysRole> {
}


@Mapper
public interface RoleMenuMapper extends BaseMapper<SysRoleMenu> {
}

@Mapper
public interface UserRoleMapper extends BaseMapper<SysUserRole> {
}

mapper实现文件

由于MenuMapper添加了一个查询权限的方法,需要在xml文件内实现,新增MenuMapper.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.cys.share.user.mapper.MenuMapper">
    <select id="listPermsByUserId" resultType="java.lang.String" parameterType="java.lang.Long">
        select distinct m.perms
        from s_sys_user_role ur
                 left join s_sys_role r on ur.role_id = r.id
                 left join s_sys_role_menu rm on ur.role_id = rm.role_id
                 left join s_sys_menu m on m.id = rm.menu_id
        where user_id = #{id}
          and r.status = 1
          and m.status = 1
    </select>
</mapper>
步骤三、在application.yml中配置mapperXML文件的位置
mybatis-plus:
  mapper-locations: classpath*:/mapper/*.xml
步骤四、开启相关配置

在配置文件SecurityConfig上面加上注解@EnableGlobalMethodSecurity, 表示开启资源访问控制

@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

在相应的资源上添加资源配置,比如上面创建的test接口,添加@PreAuthorize注解

    @PreAuthorize("hasAuthority('test')")
    @GetMapping("/test")
    public Result test() {
        return Result.success("test成功");
    }
步骤五、封装权限信息

之前我们创建的LoginUser中,getAuthorities方法是返回空的,下面是增加返回权限的全量内容

import com.alibaba.fastjson.annotation.JSONField;
import com.cys.share.user.entiy.SysUser;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    private SysUser sysUser;

    private List<String> permissions;

    public LoginUser(SysUser sysUser, List<String> permissions) {
        this.sysUser = sysUser;
        this.permissions = permissions;
    }

    /**
     * Redis默认时不会把它进行序列,但是这样会出问题
     * 其实,我们不需要把这个成员变量序列号存储到Redis当中,我们只需要存储permissions即可。
     * 我们可以将permissions转换为authorities。
     * 通过 @JSONField(serialize = false) 注解,可以不让它序列号。
     */
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities != null) {
            return authorities;
        }
        // 把permissions中String类型的权限信息封装成SimpleGrantedAuthority对象
        authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        return authorities;

    }

    @Override
    public String getPassword() {
        return sysUser.getPassword();
    }

    @Override
    public String getUsername() {
        return sysUser.getAccount();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

步骤六、UserDetailsServiceImpl添加查询权限

添加了 List list = menuMapper.listPermsByUserId(user.getId());

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MenuMapper menuMapper;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户信息
        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SysUser::getAccount, username);
        SysUser user = userMapper.selectOne(queryWrapper);
        // 如果没有查询到用户则抛出异常,在过滤链中有异常捕获,这里抛出的异常会被捕获
        if (Objects.isNull(user)) {
            throw new RuntimeException("用户名或密码错误");
        }

        //TODO 查询对应的权限信息
        // 查询对应的权限信息
        List<String> list = menuMapper.listPermsByUserId(user.getId());

        // 把数据封装成UserDetails返回
        return new LoginUser(user,list);
    }
}

步骤七、自定义失败处理

如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是无该权限导致的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
认证失败处理
新增AuthenticationEntryPointImpl文件,返回状态码 401


@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        String json = JSON.toJSONString(Result.failure(HttpStatus.UNAUTHORIZED.value(), "用户认证失败,请重新登录"));
        response.setStatus(200);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().print(json);
    }
}

无权限失败处理
新增AccessDeniedHandlerImpl 文件,返回状态码 403

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        String json = JSON.toJSONString(Result.failure(HttpStatus.FORBIDDEN.value(), "您的权限不足"));
        response.setStatus(200);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().print(json);

    }
}
步骤八、修改配置类SecurityConfig

步骤七新增了两个异常处理器,需要子配置类中添加
// 配置异常处理器
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);

@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    /**
     * 认证失败处理
     */
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    /**
     * 无权限失败处理
     */
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;


    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许访问
                .antMatchers("/login").permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        // 配置异常处理器
        http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);

    }


    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}

步骤九、修改JwtAuthenticationTokenFilter文件

校验的时候,也要获取权限。添加如下
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());

@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 id;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            id = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }

        // 从redis中获取用户信息
        String redisKey = "login:" + id;
        Object cacheObject = redisCache.getCacheObject(redisKey);
        LoginUser loginUser = JSON.parseObject(JSON.toJSONString(cacheObject),LoginUser.class);
        if (Objects.isNull(loginUser)) {
            throw new RuntimeException("用户未登录");
        }

        // 后面将需要一个Authentication的对象,在这里通过实现类UsernamePasswordAuthenticationToken构造这个对象
        // 选择3个参数的构造器,principal:账号,credentials:密码,authorities:权限
        // 为什么要选择这个构造器呢?因为这个构造器中有,super.setAuthenticated(true); 标识用户为已认证。
//        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);

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

        // 存入SecurityContextHolder
        // 存入需要一个Authentication的对象,在登录的时候也用到过类似的方法。
        // TODO 获取权限信息封装到Authentication
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        // 放行
        filterChain.doFilter(request, response);

    }
}
步骤十、授权测试

首先需要添加数据给表角色表s_sys_role
在这里插入图片描述

设置一个菜单资源test,插入到表s_sys_menu
在这里插入图片描述
绑定体验角色拥有菜单资源test的权限
在这里插入图片描述
再回顾一下用户信息,现在暂时不要给用户分配角色
在这里插入图片描述
开始进行测试
首先进行登录,登录成功
在这里插入图片描述
再拿该token去访问test接口,提示权限不足,和预期的结果一样
在这里插入图片描述
此时给用户分配对应的体验角色,拥有test资源的权限
在这里插入图片描述

然后重新进行测试,切记,需要重新登录获取新的token,才拥有新权限。测试结果如下,
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值