Springboot Spring Security +Jwt+redis+mybatisPlus 动态完成 前后端分离认证授权

Springboot Spring Security +Jwt 动态完成 前后端分离认证授权

前言

权限模型采用RBAC角色模型
框架: SpringBoot ,Spring Security ,Mybatis Plus,Swgger2,Jwt
数据库: Redis,Mysql

一.httpBasic认证 (尝鲜)

<!--security权限包-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

1.启动

简单的认证防君子不防小人

//配置类
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //可以完成基本的认证,账户默认user 密码在控制台
        //开启htttpBasic认证
        http.httpBasic()
                .and()
                //认证所有请求
                .authorizeRequests()
                //任何请求都必须认证成功
                .anyRequest()
                .authenticated();
    }
}

启动访问任何请求,需要认证

默认账号为user 密码打印在控制台

在浏览器输入路径,即可得到一次简单的认证

请添加图片描述

2.自定义httpBasic认证的账号密码

在配置文件配置即可

spring.security.user.name=admin
spring.security.user.password=admin

二.Springboot + Spring Security 前后端分离认证授权

1.准备工作

1)maven依赖
<!--安全框架-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

<!--德鲁伊连接池-->
<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.4.1</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<!--生成器-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.4.1</version>
</dependency>
<!--mybatis-plus模版引擎-->
<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.2</version>
</dependency>

<!--mvc-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</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>

 <!--redis依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.33</version>
</dependency>
<!--jwt依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

<!--swagger2 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>
2).统一异常码
/**
 * @author 志
 * @date 2022-08-25 9:10
 */
public enum  ErrorCode {
    /* 成功 */
    SUCCESS(0, "成功"),

    //失败
    FAIL(-1, "失败"),

    USER_ACCOUNT_EXPIRED(2002, "账号已过期"),
    USER_CREDENTIALS_ERROR(2003, "密码错误"),
    USER_CREDENTIALS_EXPIRED(2004, "密码过期"),
    USER_ACCOUNT_DISABLE(2005, "账号不可用"),
    USER_ACCOUNT_LOCKED(2006, "账号被锁定"),
    USER_ACCOUNT_NOT_EXIST(2007, "账号不存在"),
    LOGIN_TIME_EXPIRED(2010, "未登录或登陆已失效,请先登录"),

    /** 没有权限 */
    NO_PERMISSION(3001, "没有权限");

    private Integer code;
    private String message;

    ErrorCode(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    /**
     * 根据code获取message
     */
    public static String getMessageByCode(Integer code) {
        for (ErrorCode ele : values()) {
            if (ele.getCode().equals(code)) {
                return ele.getMessage();
            }
        }
        return null;
    }
}
3).统一返回结果
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;

@Data
@ApiModel(value = "统一返回结果")
public class WrapperResult<T> implements Serializable {
    private static final long serialVersionUID = 5778573516446596671L;

    @ApiModelProperty(value = "返回码")
    private Integer code = 0;

    @ApiModelProperty(value = "返回消息")
    private String message;

    @ApiModelProperty(value = "返回数据")
    private T data;

    public WrapperResult() {
    }
    public WrapperResult(Integer code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    /**
     * 成功
     *
     * @param data 返回数据
     * @param <T>
     * @return
     */
    public static <T> WrapperResult<T> success(T data) {
        return new WrapperResult(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMessage(), data);
    }

    /**
     * 成功
     *
     * @param data    返回数据
     * @param message 返回消息
     * @param <T>
     * @return
     */
    public static <T> WrapperResult<T> success(T data, String message) {
        message = message != null && message.length() > 0 ? message : ErrorCode.SUCCESS.getMessage();
        return new WrapperResult(ErrorCode.SUCCESS.getCode(), message, data);
    }

    /**
     * 失败
     * @param data 返回数据
     * @param <T>
     * @return
     */
    public static <T> WrapperResult<T> fail(T data) {
        return new WrapperResult(ErrorCode.FAIL.getCode(), ErrorCode.FAIL.getMessage(), data);
    }

    /**
     *
     * @param data 返回数据
     * @param message 返回消息
     * @param <T>
     * @return
     */
    public static <T> WrapperResult<T> fail(T data, String message) {
        message = message != null && message.length() > 0 ? message : ErrorCode.FAIL.getMessage();
        return new WrapperResult(ErrorCode.FAIL.getCode(), message, data);
    }
}
4).配置文件
server:
  port: 8888

spring:
  main:
    allow-bean-definition-overriding: true

## mysql数据库
  datasource:
    username: user
    password: 123
    url: jdbc:mysql://127.0.0.1:3306/myapp?serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
#在这里配置redis相关的连接   host:ip地址  port:端口   password:访问密码
  redis:
    host: 172.16.114.242
    port: 6379
    password: admin

#mybatis plus相关配置
mybatis-plus:
    #扫描mapper映射文件
    mapper-locations: classpath*:sql/**/*.xml
    #打印sql语句
    configuration:
      log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
      # 如果查询结果中包含空值的列,则 MyBatis 在映射的时候,不会映射这个字段
      call-setters-on-nulls: true
      # 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
      map-underscore-to-camel-case: true
5).jwt工具类
/**
 * JWT工具类
 */
public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 60 * 30 *1000L;// 60 * 30 *1000  30分钟
    //设置秘钥明文
    public static final String JWT_KEY = "xiaozhi";

    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("xiaozhi")     // 签发者
                .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();
    }

    /**
     * 生成加密后的秘钥 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();
    }
}
6).reids配置及工具类

redis序列化

/**
 * Redis使用FastJson序列化
 */
public class FastJsonRedisSerializer<T> implements RedisSerializer<T>
{

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;

    static
    {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJsonRedisSerializer(Class<T> clazz)
    {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException
    {
        if (t == null)
        {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
        if (bytes == null || bytes.length <= 0)
        {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz);
    }

    protected JavaType getJavaType(Class<?> clazz)
    {
        return TypeFactory.defaultInstance().constructType(clazz);
    }
}

redis工具类

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);
    }
}

redis配置类

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;
    }
}
7).全局异常处理
//全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    @ExceptionHandler(value = Exception.class)
    public WrapperResult<Object> exceptionHandler(HandlerMethod handlerMethod, Exception e){
        WrapperResult res =new WrapperResult();
        this.logger.error("调用目标方法:"+handlerMethod+"出现异常 ",e);
        res.setCode(ErrorCode.FAIL.getCode());
        res.setMessage("调用目标服务异常,请联系管理员查看后台日志信息");
        return res;
    }

    @ExceptionHandler(value = RuntimeException.class)
    public WrapperResult<Object> exceptionHandler(HandlerMethod handlerMethod, RuntimeException e){
        WrapperResult res =new WrapperResult();
        this.logger.error("调用目标方法:"+handlerMethod+"出现异常 ",e);
        res.setCode(ErrorCode.FAIL.getCode());
        res.setMessage(e.getMessage());
        return res;
    }
}
8).sql语句

数据库表模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CPSZHrct-1663312446135)(C:/Users/h/AppData/Roaming/Typora/typora-user-images/image-20220905170209976.png)]

用户表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `user_id` bigint(20) NOT NULL COMMENT '用户编号',
  `user_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名',
  `pwd` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户密码',
  `email` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `sex` varchar(2) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '性别',
  `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `user_statu` int(5) NULL DEFAULT NULL COMMENT '是否可用 0--可用  1--不可用',
  `last_login_time` datetime(0) NULL DEFAULT NULL COMMENT '上传登录时间',
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'user', '$2a$10$sPbHeIKrKIobRnvv4wpzNuwszyAFGth8OliQCWKvJNVo31bF0YRCe', NULL, NULL, '2022-05-11 13:38:41', 1, NULL);
INSERT INTO `sys_user` VALUES (2, 'user2', '$2a$10$sPbHeIKrKIobRnvv4wpzNuwszyAFGth8OliQCWKvJNVo31bF0YRCe', NULL, NULL, '2022-08-29 11:51:21', 1, NULL);

SET FOREIGN_KEY_CHECKS = 1;

角色表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `role_id` bigint(20) NOT NULL COMMENT '编号',
  `name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色名',
  `code` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色编码',
  `remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
  `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `role_statu` int(5) NULL DEFAULT NULL COMMENT '角色状态 0--可用  1--不可用',
  PRIMARY KEY (`role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, '管理员', 'admin', NULL, '2022-08-23 16:25:14', 1);
INSERT INTO `sys_role` VALUES (2, '普通用户', 'user', NULL, '2022-08-23 16:25:49', 1);

SET FOREIGN_KEY_CHECKS = 1;

菜单表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu`  (
  `menu_id` bigint(20) NOT NULL COMMENT '菜单id',
  `parent_id` bigint(20) NULL DEFAULT NULL COMMENT '父级id, 根菜单为0',
  `menu_name` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜单名',
  `srt` int(4) NULL DEFAULT 0 COMMENT '排序',
  `path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '路由地址',
  `component` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '组件路径',
  `menu_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜单类型(M目录 C菜单 F请求路径)',
  `visible` varchar(2) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
  `status` varchar(2) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
  `icon` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜单图标',
  PRIMARY KEY (`menu_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜单表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES (1, 0, '查询操作', 0, '/sys/query', NULL, 'F', '0', '0', NULL);
INSERT INTO `sys_menu` VALUES (2, 0, '删除操作', 0, '/sys/delete', NULL, 'F', '0', '0', NULL);

SET FOREIGN_KEY_CHECKS = 1;

用户和角色关联表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role`  (
  `user_role_id` bigint(20) NOT NULL COMMENT '编号',
  `user_id` bigint(20) NULL DEFAULT NULL COMMENT '用户编号',
  `role_id` bigint(20) NULL DEFAULT NULL COMMENT '角色编号',
  PRIMARY KEY (`user_role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户角色关联表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (2, 1, 1);
INSERT INTO `sys_user_role` VALUES (4, 2, 2);

SET FOREIGN_KEY_CHECKS = 1;

角色和菜单关联表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu`  (
  `role_menu_id` bigint(20) NOT NULL COMMENT '角色菜单id',
  `role_id` bigint(20) NOT NULL COMMENT '角色id',
  `menu_id` bigint(20) NOT NULL COMMENT '菜单id',
  PRIMARY KEY (`role_menu_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色菜单关联表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES (1, 1, 1);
INSERT INTO `sys_role_menu` VALUES (2, 1, 2);
INSERT INTO `sys_role_menu` VALUES (3, 2, 1);

SET FOREIGN_KEY_CHECKS = 1;

2.spring security核心配置文件

最重要的配置,像认证、授权、登入登出都在此处配置

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
     @Override
    protected void configure(HttpSecurity http) throws Exception {
        
    }
}
9).集成swgger2

swgger2依赖加一下

<!--swagger2 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

注入swgger2配置

@Configuration
@EnableSwagger2
public class Swgger2 {
    @Bean
    public Docket createRestApi() {// 创建API基本信息
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com"))// 扫描该包下的所有需要在Swagger中展示的API,@ApiIgnore注解标注的除外
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {// 创建API的基本信息,这些信息会在Swagger UI中进行显示
        return new ApiInfoBuilder()
                .title("Swagger接口文档")
                .description("Swagger-接口文档")// API描述
                .version("1.0.0")// 版本号
                .build();
    }
}

配置swgger2访问白名单,这个类为spring security 核心配置类

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //白名单,访问有以下路径不需要登录
    private static final String[] whitelist = {
            , "/swagger-ui.html"
            , "/swagger-resources/**"
            , "/webjars/springfox-swagger-ui/**"
            , "/v2/api-docs"
    };

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

启动服务在浏览器输入地址,在端口后面加上 /swagger-ui.html,出现以下页面代表成功

请添加图片描述

swagger的介绍: swagger常见问题和介绍
10).实体类和mybatis映射文件
①用戶表

SysUser.java

@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value="SysUser对象", description="用户表")
@TableName(value = "sys_user")
public class SysUser implements Serializable{

    private static final long serialVersionUID = 1L;

    @TableId
    @ApiModelProperty(value = "用户编号")
    private Long userId;

    @ApiModelProperty(value = "用户名")
    private String userName;

    @ApiModelProperty(value = "用户密码")
    private String pwd;

    @ApiModelProperty(value = "邮箱")
    private String email;

    @ApiModelProperty(value = "性别")
    private String sex;

    @ApiModelProperty(value = "创建时间")
    private Date createTime;

    @ApiModelProperty(value = "是否可用 0--可用  1--不可用")
    private Integer userStatu;

    @ApiModelProperty("上一次登录时间")
    private Date lastLoginTime;

    @TableField(exist = false)
    private String token;

    @TableField(exist = false)
    private List<SysRole> roles;
}

SysUserDao.java

public interface SysUserDao extends BaseMapper<SysUser> {
    SysUser queryOneLoginUser(String username);
}

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.example.demobiz.corp.sys.user.dao.SysUserDao">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.example.demobiz.corp.sys.user.entity.SysUser">
        <id column="user_id" property="userId" />
        <result column="user_name" property="userName" />
        <result column="pwd" property="pwd" />
        <result column="email" property="email" />
        <result column="sex" property="sex" />
        <result column="create_time" property="createTime" />
        <result column="user_statu" property="userStatu" />
        <result column="last_login_time" property="lastLoginTime" />
    </resultMap>



    <resultMap id="userMap" type="com.example.demobiz.corp.sys.user.entity.SysUser">
        <id column="user_id" property="userId" />
        <result column="user_name" property="userName" />
        <result column="pwd" property="pwd" />
        <result column="email" property="email" />
        <result column="sex" property="sex" />
        <result column="create_time" property="createTime" />
        <result column="user_statu" property="userStatu" />
        <collection property="roles" resultMap="com.example.demobiz.corp.sys.role.dao.SysRoleDao.BaseResultMap"></collection>
    </resultMap>

    <select id="queryOneLoginUser" resultMap="userMap">
           SELECT
            u.user_id,
            u.user_name,
            u.pwd,
            u.email,
            u.sex,
            u.create_time,
            u.user_statu,
            r.role_id,
            r.`name`,
            r.`code`,
            r.remark,
            r.role_statu
        FROM
            sys_user u
            INNER JOIN sys_user_role ur ON u.user_id = ur.user_id
            INNER JOIN sys_role r ON r.role_id = ur.role_id
        WHERE
            r.role_statu =1  and user_name = #{username}
    </select>
</mapper>

②角色表

SysRole.java

@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value="SysRole对象", description="角色表")
public class SysRole implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "编号")
    private Long roleId;

    @ApiModelProperty(value = "角色名")
    private String name;

    @ApiModelProperty(value = "角色编码")
    private String code;

    @ApiModelProperty(value = "备注")
    private String remark;

    @ApiModelProperty(value = "创建时间")
    private Date createTime;

    @ApiModelProperty(value = "角色状态 0--可用  1--不可用")
    private Integer roleStatu;
}

SysRoleDao.java

public interface SysRoleDao extends BaseMapper<SysRole> {}

SysRoleMapper.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.example.demobiz.corp.sys.role.dao.SysRoleDao">
    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.example.demobiz.corp.sys.role.entity.SysRole">
        <id column="role_id" property="roleId" />
        <result column="name" property="name" />
        <result column="code" property="code" />
        <result column="remark" property="remark" />
        <result column="create_time" property="createTime" />
        <result column="role_statu" property="roleStatu" />
    </resultMap>
</mapper>

③菜单表

SysMenu.java

@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value="SysMenu对象", description="菜单表")
public class SysMenu implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "菜单id")
    private Long menuId;

    @ApiModelProperty(value = "父级id, 根菜单为0")
    private Long parentId;

    @ApiModelProperty(value = "菜单名")
    private String menuName;

    @ApiModelProperty(value = "排序")
    private Integer srt;

    @ApiModelProperty(value = "路由地址")
    private String path;

    @ApiModelProperty(value = "组件路径")
    private String component;

    @ApiModelProperty(value = "菜单类型(M目录 C菜单 F按钮)")
    private String menuType;

    @ApiModelProperty(value = "菜单状态(0显示 1隐藏)")
    private String visible;

    @ApiModelProperty(value = "菜单状态(0正常 1停用)")
    private String status;

    @ApiModelProperty(value = "菜单图标")
    private String icon;

    @TableField(exist = false)
    private List<SysRole> roles;
}

SysMenuDao.java

public interface SysMenuDao extends BaseMapper<SysMenu> {
        /**
     * 根据请求路径查询菜单对应的权限
     */
    List<SysMenu> queryMenuRoles(String path);
}

SysMenuMapper.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.example.demobiz.corp.sys.menu.dao.SysMenuDao">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.example.demobiz.corp.sys.menu.entity.SysMenu">
        <id column="menu_id" property="menuId" />
        <result column="parent_id" property="parentId" />
        <result column="menu_name" property="menuName" />
        <result column="srt" property="srt" />
        <result column="path" property="path" />
        <result column="component" property="component" />
        <result column="menu_type" property="menuType" />
        <result column="visible" property="visible" />
        <result column="status" property="status" />
        <result column="icon" property="icon" />
    </resultMap>

    <resultMap id="queryMenuRolesMap"  type="com.example.demobiz.corp.sys.menu.entity.SysMenu">
        <id column="menu_id" property="menuId" />
        <result column="parent_id" property="parentId" />
        <result column="menu_name" property="menuName" />
        <result column="srt" property="srt" />
        <result column="path" property="path" />
        <result column="component" property="component" />
        <result column="menu_type" property="menuType" />
        <result column="visible" property="visible" />
        <result column="status" property="status" />
        <result column="icon" property="icon" />
        <collection property="roles" resultMap="com.example.demobiz.corp.sys.role.dao.SysRoleDao.BaseResultMap"></collection>
    </resultMap>

    <select id="queryMenuRoles" parameterType="string" resultMap="queryMenuRolesMap">
        SELECT
            m.menu_id,
            m.menu_name,
            m.path,
            m.menu_type,
            r.role_id,
            r.`name`,
            r.`code`
        FROM
            sys_menu m
            INNER JOIN sys_role_menu rm ON m.menu_id = rm.menu_id
            INNER JOIN sys_role r ON r.role_id = rm.role_id
            where m.menu_type='F'  and m.path= #{path}
    </select>
</mapper>

2.登录认证

1).创建 UserDetailsServiceImpl并且根据用户名查询用户信息和对应的权限

这是用户认证的核心逻辑,创建UserDetailsServiceImpl并且实现UserDetailsService接口重写loadUserByUsername方法

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserDao dao;

    @Override
    public UserDetails loadUserByUsername(String username) throws InternalAuthenticationServiceException {
        SysUser sysUser = dao.queryOneLoginUser(username);
        if (sysUser == null) {
            throw new InternalAuthenticationServiceException("用户名不存在");
        }
        //TODO 查询权限对应的信息
        List<String> list = new ArrayList<>();
        for (SysRole role : sysUser.getRoles()) {
            list.add(role.getCode());
        }
        return new LoginUser(sysUser,list);
    }
}
2)登录用户信息

创建LoginUser实现UserDetails接口

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
	//用户信息
    private SysUser sysUser;
	//用户权限
    private List<String> permissions;

    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        String[] strings = permissions.toArray(new String[permissions.size()]);
        if (authorities != null) {
            return authorities;
        }
        authorities = AuthorityUtils.createAuthorityList(strings);
        return authorities;
    }

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

    @Override
    public String getUsername() {
        return sysUser.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;
    }
}
3)配置认证管理且配置密码加密

核心配置中配置 WebSecurityConfig

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

注意这里几步出现StackOverflowError错误请参考

4)配置密码加密

新版本的Spring security规定必须设置一个默认的加密方式,不允许使用明文。这个加密方式是用于在登录时验证密码、注册时需要用到。
    我们可以自己选择一种加密方式,Spring security为我们提供了多种加密方式,我们这里使用一种强hash方式进行加密。

请添加图片描述

在WebSecurityConfig 中注入(注入即可,不用声明使用),这样就会对提交的密码进行加密处理了,如果你没有注入加密方式,运行的时候会报错"There is no PasswordEncoder mapped for the id"错误。

@Bean
public PasswordEncoder passwordEncoder() {
    //这里选择的是不可逆哈希算法
    return new BCryptPasswordEncoder();
}
5)创建SecurityUtils工具获取用户登录信息
public class SecurityUtils {
    /**
     * 获取 Authentication
     */
    public static Authentication getAuthentication() {
        return SecurityContextHolder.getContext().getAuthentication();
    }

    /**
     * 获取用户信息
     */
    public static LoginUser getLoginUser(){
        try {
            return (LoginUser) getAuthentication().getPrincipal();
        }catch (Exception e){
            throw new RuntimeException("获取用户信息异常");
        }
    }
}
6)JWT令牌校验
①创建JwtAuthenticationTokenFilter
@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 uuid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            uuid = claims.getSubject();
        } catch (Exception e) {
            request.setAttribute("filterError",  new RuntimeException(ErrorCode.LOGIN_TIME_EXPIRED.getMessage()));
            request.getRequestDispatcher("/error/throw").forward(request,response);
            return;
        }
        //从redis中获取用户信息
        String redisKey = "login:" + uuid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if (Objects.isNull(loginUser)) {
            request.setAttribute("filterError",  new RuntimeException(ErrorCode.LOGIN_TIME_EXPIRED.getMessage()));
            request.getRequestDispatcher("/error/throw").forward(request,response);
            return;
        }

        //存入SecurityContextHolder
        //TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}
②过滤器异常重定向

因为过滤器抛出的异常全局异常捕抓不到所以创建ThrowError用于抛出异常

@Controller
public class ThrowError {
    @RequestMapping("/error/throw")
    public void rethrow(HttpServletRequest request) {
        throw (RuntimeException) request.getAttribute("filterError");
    }
}
③配置拦截

核心配置WebSecurityConfig

@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
 @Override
 protected void configure(HttpSecurity http) throws Exception {
      //指定jwtAuthenticationTokenFilter过滤器在UsernamePasswordAuthenticationFilter之前
      http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
 }
7)自定义登录策略
public class UserAndPwdLoginFilter extends UsernamePasswordAuthenticationFilter {

    @Resource
    private SessionRegistry sessionRegistry;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)
                || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
            ObjectMapper mapper = new ObjectMapper();
            UsernamePasswordAuthenticationToken authRequest = null;
            try (InputStream is = request.getInputStream()) {
                Map<String,String> authenticationBean = mapper.readValue(is, Map.class);
                String userName =authenticationBean.get("userName");
                String pwd =authenticationBean.get("pwd");
                if (userName == null) {
                    userName = "";
                }

                if (pwd == null) {
                    pwd = "";
                }
                request.setAttribute("userName",userName);

                authRequest = new UsernamePasswordAuthenticationToken(
                        userName, pwd);

                sessionRegistry.registerNewSession(request.getSession().getId(),authRequest.getPrincipal());
            } catch (IOException e) {
                e.printStackTrace();
                authRequest = new UsernamePasswordAuthenticationToken(
                        "", "");
            }
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
        else {
            return super.attemptAuthentication(request, response);
        }
    }
}

核心配置WebSecurityConfig

@Resource
private SessionRegistry sessionRegistry;

 @Bean
public SessionRegistry sessionRegistry() {
    return new SessionRegistryImpl();
}

@Autowired
private RedisCache redisCache;

 /**
     * 账号密码登录处理
     */
@Bean
UserAndPwdLoginFilter userAndPwdSuccessFilter() throws Exception {
    UserAndPwdLoginFilter filter = new UserAndPwdLoginFilter();
    filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
            LoginUser loginUser = (LoginUser) authentication.getPrincipal();
            String uuid= UUID.randomUUID().toString();
            String jwt = JwtUtil.createJWT(uuid);
            //authenticate存入redis
            redisCache.setCacheObject("login:" + uuid, loginUser,10, TimeUnit.MINUTES);
            //把token响应给前端
            WrapperResult<Object> result = WrapperResult.success(jwt, "登录成功");
            httpServletResponse.setContentType("text/json;charset=utf-8");
            //塞到HttpServletResponse中返回给前台
            httpServletResponse.getWriter().write(JSON.toJSONString(result));
        }
    });

    filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
            //返回json数据
            WrapperResult result = null;
            if (e instanceof AccountExpiredException) {
                //账号过期
                result = WrapperResult.fail(null, ErrorCode.USER_ACCOUNT_EXPIRED.getMessage());
            } else if (e instanceof BadCredentialsException) {
                //密码错误
                result = WrapperResult.fail(null, ErrorCode.USER_CREDENTIALS_ERROR.getMessage());
            } else {
                //其他错误
                result = WrapperResult.fail(null, e.getMessage());
            }
            //处理编码方式,防止中文乱码的情况
            response.setContentType("text/json;charset=utf-8");
            //塞到HttpServletResponse中返回给前台
            response.getWriter().write(JSON.toJSONString(result));
        }
    });

    filter.setAuthenticationManager(authenticationManagerBean());
    filter.setSessionAuthenticationStrategy(new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry));
    return filter;
}

configure加

@Override
protected void configure(HttpSecurity http) throws Exception {
      //登录处理逻辑(账户密码登录)
      http.addFilterAfter(userAndPwdSuccessFilter(), UsernamePasswordAuthenticationFilter.class);
}
8)用户未登录处理逻辑

创建CustomizeAuthenticationEntryPoint

/**
 * @author 志
 * @date 2022-08-25 9:03
 * 匿名用户访问无权限资源时的异常
 */
@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        /**
         * 用户未登录访问没有配置白名单的路径
         */
        WrapperResult wrapperResult = WrapperResult.fail(null, ErrorCode.LOGIN_TIME_EXPIRED.getMessage());
        httpServletResponse.setContentType("text/json;charset=utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(wrapperResult));
    }
}

核心配置WebSecurityConfig

/**
     * 用户未登录处理逻辑
     * @return
*/
@Autowired
CustomizeAuthenticationEntryPoint customizeAuthenticationEntryPoint;

configure加

   http
       //关闭csrf
       .csrf().disable()
       //不通过Session获取SecurityContext
       .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
       .and().authorizeRequests()
       // 对于登录接口 允许匿名访问
       .antMatchers(whitelist).anonymous()
       // 除上面外的所有请求全部需要鉴权认证
       .anyRequest().authenticated()
       
       //新加--------------------------------------
       .and().exceptionHandling()
       //未登录访问
       .authenticationEntryPoint(customizeAuthenticationEntryPoint)
;

3.授权

1)注解权限配置
①处理类
/**
 * 自定义权限
 */
@Service("zhi")
public class Permission {

    /**
     * @param permission
     * @return
     */
    public boolean hasPermission(String permission) {
        if (StringUtils.isEmpty(permission)) {
            return false;
        }
        LoginUser loginUser= SecurityUtils.getLoginUser();
        if (loginUser == null || CollectionUtils.isEmpty(loginUser.getPermissions()))  {
            return false;
        }
        return loginUser.getPermissions().contains(permission);
    }
}
②使用

调用delete需要admin角色权限

@RestController
@Api(tags = "用户信息")
@RequestMapping("/sys")
public class SysUserController {

    @RequestMapping("/query")
    public WrapperResult<Object> query(){
        return WrapperResult.success(null, "query");
    }

    @PreAuthorize("@zhi.hasPermission('admin')")//打上这个注解
    @RequestMapping("/delete")
    public WrapperResult<Object> delete(){
        return WrapperResult.success(null, "delete");
    }
}
2)授权失败逻辑处理配置

创建CustomAccessDeniedHandler

/**
 * @author 志
 * @date 2022-09-05 9:25
 * @Description: 自定义权限不足处理
 */
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        WrapperResult result = WrapperResult.fail(null, accessDeniedException.getMessage());
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(result));
    }
}

核心配置WebSecurityConfig

    //自定义授权失败处理
    @Autowired
    CustomAccessDeniedHandler customAccessDeniedHandler;

configure

 http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers(whitelist).anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and().exceptionHandling()
                //未登录访问
                .authenticationEntryPoint(customizeAuthenticationEntryPoint)
     			//新加--------------------------------------
                //权限不足
                .accessDeniedHandler(customAccessDeniedHandler)
        ;
3)自定义权限拦截

注解配置权限固然方便,但是在实际开发中不可能在每个需要授权的方法上去配置,工作量大,同时在权限变更的时,还需要改动代码发版本,自定义权限拦截这是最好的处理方式,将权限配置存在数据库,然后拦截检查用户是否符合权限规则

①安全元数据源
@Component
public class CustomizeFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Autowired
    private SysMenuDao sysMenuDao;

    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        //获取请求地址
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        //查询具体某个接口的权限
        List<SysMenu> sysMenus = sysMenuDao.queryMenuRoles(requestUrl);
        if(CollectionUtils.isEmpty(sysMenus)){
            //请求路径没有配置权限,表明该请求接口可以任意访问
            return null;
        }
        //菜单对应的权限
        List<SysRole> roles = sysMenus.get(0).getRoles();
        String[] attributes = new String[roles.size()];

        for(int i = 0;i<roles.size();i++){
            attributes[i] = roles.get(i).getCode();
        }
        return SecurityConfig.createList(attributes);
    }


    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}
②访问决策管理器
@Component
public class CustomizeAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        Iterator<ConfigAttribute> iterator = collection.iterator();
        while (iterator.hasNext()) {
            ConfigAttribute ca = iterator.next();
            //当前请求需要的权限
            String needRole = ca.getAttribute();
            //当前用户所具有的权限
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足!");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

③配置

核心配置WebSecurityConfig

    //访问决策管理器
    @Autowired
    CustomizeAccessDecisionManager accessDecisionManager;

    //安全元数据源
    @Autowired
    CustomizeFilterInvocationSecurityMetadataSource securityMetadataSource;

configure加

 //授权配置
http.authorizeRequests().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
    @Override
    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
        o.setAccessDecisionManager(accessDecisionManager);//访问决策管理器
        o.setSecurityMetadataSource(securityMetadataSource);//安全元数据源
        return o;
    }
});
4)退出登录逻辑处理

退出登录一般将session,redis,token删除或者失效

创建CustomizeLogoutSuccessHandler

@Component
public class CustomizeLogoutSuccessHandler implements LogoutSuccessHandler {

    @Autowired
    private RedisCache redisCache;

    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        String token = httpServletRequest.getHeader("token");
        //解析token
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
            redisCache.deleteObject("login:"+userid);
        } catch (Exception e) {
            e.printStackTrace();
        }
        WrapperResult result = WrapperResult.success(null,"退出成功");
        httpServletResponse.setContentType("text/json;charset=utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

核心配置WebSecurityConfig

/**
     * 退出登录处理逻辑
     */
    @Autowired
    CustomizeLogoutSuccessHandler customizeLogoutSuccessHandler;

configure

//设置退出登录处理器
http.logout()//默认退出
    .logoutSuccessHandler(customizeLogoutSuccessHandler)
    //登出之后删除cookie
    .deleteCookies("JSESSIONID");

三.最终WebSecurityConfig的配置

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

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Resource
    private SessionRegistry sessionRegistry;

    @Autowired
    private RedisCache redisCache;

    /**
     * 用户未登录处理逻辑
     * @return
     */
    @Autowired
    CustomizeAuthenticationEntryPoint customizeAuthenticationEntryPoint;

    /**
     * 退出登录处理逻辑
     */
    @Autowired
    CustomizeLogoutSuccessHandler customizeLogoutSuccessHandler;

    //访问决策管理器
    @Autowired
    CustomizeAccessDecisionManager accessDecisionManager;

    //安全元数据源
    @Autowired
    CustomizeFilterInvocationSecurityMetadataSource securityMetadataSource;

    //自定义授权失败处理
    @Autowired
    CustomAccessDeniedHandler customAccessDeniedHandler;

    //白名单
    private static final String[] whitelist = {
            "/login"
            , "/swagger-ui.html"
            , "/swagger-resources/**"
            , "/webjars/springfox-swagger-ui/**"
            , "/v2/api-docs"
    };

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers(whitelist).anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and().exceptionHandling()
                //未登录访问
                .authenticationEntryPoint(customizeAuthenticationEntryPoint)
                //权限不足
                .accessDeniedHandler(customAccessDeniedHandler)
        ;

    //认证授权
        http.authorizeRequests().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
        @Override
        public <O extends FilterSecurityInterceptor> O postProcess(O o) {
            o.setAccessDecisionManager(accessDecisionManager);//访问决策管理器
            o.setSecurityMetadataSource(securityMetadataSource);//安全元数据源
            return o;
        }
    });

    //登录处理逻辑(账户密码登录)
        http.addFilterAfter(userAndPwdSuccessFilter(), UsernamePasswordAuthenticationFilter.class);

    //指定jwtAuthenticationTokenFilter过滤器在UsernamePasswordAuthenticationFilter之前
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

    //设置退出登录处理器
        http.logout()//默认退出/
                .logoutSuccessHandler(customizeLogoutSuccessHandler)
    //登出之后删除cookie
                .deleteCookies("JSESSIONID");
}

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

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


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


    /**
     * 账号密码登录处理
     */
    @Bean
    UserAndPwdLoginFilter userAndPwdSuccessFilter() throws Exception {
        UserAndPwdLoginFilter filter = new UserAndPwdLoginFilter();
        filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                LoginUser loginUser = (LoginUser) authentication.getPrincipal();
                String uuid= UUID.randomUUID().toString();
                String jwt = JwtUtil.createJWT(uuid);
                //authenticate存入redis
                redisCache.setCacheObject("login:" + uuid, loginUser,10, TimeUnit.MINUTES);
                //把token响应给前端
                WrapperResult<Object> result = WrapperResult.success(jwt, "登录成功");
                httpServletResponse.setContentType("text/json;charset=utf-8");
                //塞到HttpServletResponse中返回给前台
                httpServletResponse.getWriter().write(JSON.toJSONString(result));
            }
        });

        filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
                //返回json数据
                WrapperResult result = null;
                if (e instanceof AccountExpiredException) {
                    //账号过期
                    result = WrapperResult.fail(null, ErrorCode.USER_ACCOUNT_EXPIRED.getMessage());
                } else if (e instanceof BadCredentialsException) {
                    //密码错误
                    result = WrapperResult.fail(null, ErrorCode.USER_CREDENTIALS_ERROR.getMessage());
                } else if (e instanceof CredentialsExpiredException) {
                    //密码过期
                    result = WrapperResult.fail(null, ErrorCode.USER_CREDENTIALS_EXPIRED.getMessage());
                } else if (e instanceof DisabledException) {
                    //账号不可用
                    result = WrapperResult.fail(null, ErrorCode.USER_ACCOUNT_DISABLE.getMessage());
                } else if (e instanceof LockedException) {
                    //账号锁定
                    result = WrapperResult.fail(null, ErrorCode.USER_ACCOUNT_LOCKED.getMessage());
                } else if (e instanceof InternalAuthenticationServiceException) {
                    //用户不存在
                    result = WrapperResult.fail(null, ErrorCode.USER_ACCOUNT_NOT_EXIST.getMessage());
                } else {
                    //其他错误
                    result = WrapperResult.fail(null, e.getMessage());
                }
                //处理编码方式,防止中文乱码的情况
                response.setContentType("text/json;charset=utf-8");
                //塞到HttpServletResponse中返回给前台
                response.getWriter().write(JSON.toJSONString(result));
            }
        });

        filter.setAuthenticationManager(authenticationManagerBean());
        filter.setSessionAuthenticationStrategy(new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry));
        return filter;
    }
}

四.测试

1.登录过期或者未登录访问,需要认证请求

没传token,或者token过期或者redis存储过期
在这里插入图片描述

2.登录成功

账号密码正确并返回token信息
在这里插入图片描述

3.账号不存在

在这里插入图片描述

4.授权访问

携带token访问已授权的请求
在这里插入图片描述


访问未授权的请求
在这里插入图片描述

5.退出登录

将登录的token,调用退出登录
在这里插入图片描述

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
基于SpringBoot2、MyBatisPlus、Spring Security5.7、JWTRedis的开发框架可以提供以下功能和优势: 1. Spring Boot2是一个轻量级的Java开发框架,能够快速构建Web应用程序和微服务。它提供了自动配置和约定大于配置的设计理念,减少了开发的复杂性。 2. MyBatisPlus是一个在MyBatis基础上进行扩展的ORM框架,提供了更简洁、更便捷的数据库访问方式。它支持代码生成、自动SQL映射、分页查询等功能,能够进一步提高开发效率。 3. Spring Security5.7是一个基于Spring的身份认证授权框架,可以进行用户认证、角色授权、API权限控制等。它提供了一套完整的解决方案,保护应用程序免受各种安全威胁。 4. JWT(Json Web Token)是一种用于跨网络进行身份验证的开放标准。它使用JSON对象作为令牌,可以在客户端和服务器之间传递信息。JWT具有无状态、可扩展、跨平台等特点,适用于分布式系统和移动应用程序。 5. Redis是一种高性能的键值存储系统,它支持数据持久化、集群模式、发布订阅等功能。在开发过程中,可以使用Redis存储JWT令牌、缓存数据等,提高系统的性能和可扩展性。 综上所述,基于SpringBoot2、MyBatisPlus、Spring Security5.7、JWTRedis的开发框架具有快速开发、高效数据库访问、可靠的安全保护和可扩展的分布式支持等优势。它可以帮助开发者快速构建稳定、安全、高性能的Web应用程序和微服务。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值