SpringBoot SpringSecurity JWT+Redis+RSA授权登录登出 验证码 前后端分离 分布式

目录

一、案例前的准备

1、RSA公钥私钥生成

2、JWT通过RSA算法签发

3、Redis的配置

4、FastJson配置

5、用户表与实体类

6、统一返回对象

7、常量类

8、图片验证码的使用

9、雪花算法生成唯一id

10、web渲染工具类

二、SpringSecurity的配置

1、SpringSecurity配置类

2、SpringSecurity异常处理器

三、JWT+SpringSecurity实现登录的主要步骤

1、登录图解

2、登录开始前首先创建UserDetails实体类的子类

3、创建UserDetailsService的实现类

4、登录的实现类与接口

5、认证过滤器

四、测试

1、登录测试

 2、资源访问测试

 3、退出登录

 五、案例源码链接


一、案例前的准备

1、RSA公钥私钥生成

第一步:生成密钥

通过keyTool(java提供的证书管理工具)生成
只要安装了JDK,就有这个工具。进入jdk的安装目录,就能使用该工具:

输入以下命令生成私钥:

// 以下指令为一行,设置多行是方便阅读
keytool -genkeypair 
-alias 密钥别名 
-keyalg 使用的算法 
-keypass 密钥的访问密码 
-keystore 生成的密钥库文件名,扩展名是jks
-storepass 密钥库的访问密码,用来打开jks文件

此时私钥已生成:

 

第二步:生成公钥 

对于微服务来说,私钥放在认证服务上,其他服务只需要存公钥即可。因为其他服务只做校验,不加密JWT。私钥加密,公钥解密。

生成公钥输入以下命令:

keytool -list -rfc --keystore jks文件(包含扩展名.jks) | openssl x509 -inform pem -pubkey

复制文本,重命名文件

 

 公钥和私钥放到服务器上,私钥用于授权服务器授权token时加密,其他服务器仅需要通过公钥便可对加密内容进行解析了。

2、JWT通过RSA算法签发

这里需要添加依赖:

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-rsa</artifactId>
            <version>1.0.11.RELEASE</version>
        </dependency>

 JWT工具类:

package com.dragonwu.utils;

import com.dragonwu.model.OnlineUser;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.Map;
import java.util.Objects;

/**
 * 通过RSA非对称加密的JWT工具类
 *
 * @author DragonWu
 * @date 2022-10-06 17:51
 **/
public class JwtUtil {

    //默认60分钟
    public static final Long DEFAULT_EXPIRE_MINUTES = 60L;

    //jwt的唯一标识,一般为username或userId
    public static final String JWT_PRIMARY_KEY = "userId";

    //私钥路径
    public static final String PRIVATE_KEY_PATH = "/keys/dragonwu.jks";
    //公钥路径
    public static final String PUBLIC_KEY_PATH = "/keys/public.key";

    //私钥
    public static PrivateKey privateKey;
    //公钥
    public static PublicKey publicKey;

    /**
     * 传入的数据不得含敏感信息,否则可能被盗用,私钥加密token
     *
     * @param claims  数据体
     * @param minutes 过期时间,分
     * @return String   JWT的token
     */
    public static String createJwt(Map<String, Object> claims, long minutes) {
        //获取私钥
        if (Objects.isNull(privateKey)) {
            privateKey = getPrivateKey();
        }

        //当前系统时间
        long now = System.currentTimeMillis();
        //过期时间
        long exp = now + minutes * 60 * 1000;

        JwtBuilder jwtBuilder = Jwts.builder()
                //签发的算法
                .signWith(SignatureAlgorithm.RS256, privateKey)
                //设置令牌过期时间
                .setExpiration(new Date(exp))
                //设置签发时间
                .setIssuedAt(new Date())
                //设置body数据,自定义设置
                .setClaims(claims);
        return jwtBuilder.compact();
    }

    /**
     * 公钥解析jwt
     *
     * @param jwt jwt字符串
     * @return 返回null证明解析异常
     */
    public static Map<String, Object> parseJwt(String jwt) {
        if (Objects.isNull(publicKey)) {
            publicKey = getPublicKey();
        }

        try {
            return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(jwt).getBody();
        } catch (Exception ignored) {
        }
        return null;
    }

    /**
     * 获取私钥
     *
     * @return 当获取异常时返回null
     */
    private static PrivateKey getPrivateKey() {
        try {
            ClassPathResource keyFileResource = new ClassPathResource(PRIVATE_KEY_PATH);
            //创建秘钥工厂,参数为:秘钥文件、秘钥库密码
            //import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;
            //pom文件:
            /*
             公钥私钥生成 https://blog.csdn.net/qq_37470815/article/details/123027798
             <dependency>
             <groupId>org.springframework.security</groupId>
             <artifactId>spring-security-rsa</artifactId>
             <version>1.0.11.RELEASE</version>
             </dependency>
             KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(keyFileResource, "密钥库密码password".toCharArray());
             //获取秘钥,参数为:别名,秘钥密码
             KeyPair keyContent = keyStoreKeyFactory.getKeyPair("alias别名", "秘钥的密码".toCharArray());
             */
            KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(keyFileResource, "dragonwu".toCharArray());
            KeyPair keyContent = keyStoreKeyFactory.getKeyPair("dragonwu", "dragonwu".toCharArray());
            return keyContent.getPrivate();
        } catch (Exception ignored) {
        }
        return null;
    }

    /**
     * 获取公钥
     *
     * @return 当获取异常时返回null
     */
    private static PublicKey getPublicKey() {
        try {
            Resource publicKey = new ClassPathResource(PUBLIC_KEY_PATH);
            InputStreamReader publicKeyIs = new InputStreamReader(publicKey.getInputStream());
            BufferedReader publicKeyBr = new BufferedReader(publicKeyIs);
            StringBuilder publicKeySb = new StringBuilder();
            String line;
            //将文件中的多行变为一行
            while ((line = publicKeyBr.readLine()) != null) {
                publicKeySb.append(line);
            }

            //将String转换成java的PublicKey对象
            byte[] byteKey = Base64.getDecoder().decode(publicKeySb.toString());
            X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(byteKey);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return keyFactory.generatePublic(x509EncodedKeySpec);
        } catch (Exception ignored) {
        }
        return null;
    }

    /*
    SpringSecurity登录后获取用户信息
     */
    public static OnlineUser getUser() {
        return (OnlineUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }

}

3、Redis的配置

引入依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

redis配置类:

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        // 创建 RedisTemplate 对象
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 设置连接工厂
        redisTemplate.setConnectionFactory(connectionFactory);
        // 设置 Key 的序列化 - String 序列化 RedisSerializer.string() => StringRedisSerializer.UTF_8
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        // 设置 Value 的序列化 - JSON 序列化 RedisSerializer.json() => GenericJackson2JsonRedisSerializer
        redisTemplate.setValueSerializer(RedisSerializer.json());
        redisTemplate.setHashValueSerializer(RedisSerializer.json());
        // 返回
        return redisTemplate;
    }
}

redis工具类:

@Component
public class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /*
    设置对象
     */
    public void setObject(String key, Object object, long time, TimeUnit timeUnit) {
        try {
            redisTemplate.opsForValue().set(key, object, time, timeUnit);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /*
    获取对象
     */
    public Object getObject(String key) {
        try {
            return redisTemplate.opsForValue().get(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /*
    删除对象
     */
    public void deleteObject(String key) {
        try {
            redisTemplate.delete(key);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

4、FastJson配置

引入依赖:

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.9</version>
        </dependency>

fastJson配置类:

@Configuration
public class MyFastJsonConfig {
    @Bean
    public HttpMessageConverters fastJsonHttpMessageConverters() {
        FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
        fastConverter.setFastJsonConfig(fastJsonConfig);
        return new HttpMessageConverters((HttpMessageConverter<?>) fastConverter);
    }

}

5、用户表与实体类

CREATE TABLE `sys_user` (
  `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `user_name` varchar(30) NOT NULL COMMENT '用户账号',
  `password` varchar(100) DEFAULT '' COMMENT '密码'
  PRIMARY KEY (`user_id`) USING BTREE
) ;

实体类:

@Data
@TableName("sys_user")
public class SysUser implements Serializable {
    @TableId(value = "user_id")
    private Long userId;

    private String userName;

    //对该字段不进行序列化
    @JSONField(serialize = false)
    private String password;
}

登录成功返回该前端的对象:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginVo implements Serializable {
    //用户对象
    private SysUser sysUser;

    //用户权限
    private List<String> authorities;

    //JWT token
    private String token;
}

6、统一返回对象

package com.dragonwu.model;

import com.dragonwu.constant.Constants;

import java.io.Serializable;

public class R<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 成功
     */
    public static final int SUCCESS = Constants.SUCCESS;

    /**
     * 失败
     */
    public static final int FAIL = Constants.FAIL;

    private int code;//状体码

    private String msg;//信息

    private T data;//数据

    public static <T> R<T> ok() {
        return restResult(null, SUCCESS, null);
    }

    public static <T> R<T> ok(T data) {
        return restResult(data, SUCCESS, null);
    }

    public static <T> R<T> ok(T data, String msg) {
        return restResult(data, SUCCESS, msg);
    }

    public static <T> R<T> fail() {
        return restResult(null, FAIL, null);
    }

    public static <T> R<T> fail(String msg) {
        return restResult(null, FAIL, msg);
    }

    public static <T> R<T> fail(T data) {
        return restResult(data, FAIL, null);
    }

    public static <T> R<T> fail(T data, String msg) {
        return restResult(data, FAIL, msg);
    }

    public static <T> R<T> fail(int code, String msg) {
        return restResult(null, code, msg);
    }

    public static <T> R<T> restResult(T data, int code, String msg) {
        R<T> apiResult = new R<>();
        apiResult.setCode(code);
        apiResult.setData(data);
        apiResult.setMsg(msg);
        return apiResult;
    }

    public int getCode() {
        return code;
    }

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

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

7、常量类

package com.dragonwu.constant;

/**
 * @author DragonWu
 * @since 2022-10-07 19:21
 **/
public interface Constants {
    /*
     成功
     */
    Integer SUCCESS = 200;

    /*
     失败
     */
    Integer FAIL = 500;

    /*
    redis的统一前缀
     */
    String REDIS_PRE = "DragonWu:";

    /*
    redis的登录用户前缀
     */
    String ONLINE_PRE = REDIS_PRE + "online:";

    /*
     登录
     */
    String LOGIN_OK = "登录成功";
    String LOGIN_FAIL = "登录失败";
    String LOGOUT_OK = "登出成功";
    String LOGOUT_FAIL = "登出失败";
    String LOGIN_ERROR = "用户名或密码错误";

    /*
    获取资源
     */
    String GET_RESOURCE_OK = "获取成功";
    String GET_RESOURCE_FAIL = "获取失败";


}

8、图片验证码的使用

依赖引入:

        <!--验证码-->
        <dependency>
            <groupId>com.github.whvcse</groupId>
            <artifactId>easy-captcha</artifactId>
            <version>1.6.2</version>
        </dependency>

服务层接口:

package com.dragonwu.service;

import com.dragonwu.constant.Constants;

import java.io.IOException;
import java.util.Map;

/**
 * 验证码类
 * https://github.com/whvcse/EasyCaptcha
 */
public interface EasyCaptchaService {
    /*
     验证码宽度
     */
    Integer CAPTCHA_WIDTH = 130;

    /*
    验证码高度
     */
    Integer CAPTCHA_HEIGHT = 48;
    /*
     验证码长度
     */
    Integer CAPTCHA_LENGTH = 4;

    /*
     验证码类型: 默认图片
     */
    int CAPTCHA_TYPE_DEFAULT_PNG = 10;
    /*
     验证码类型: 动态GIF
     */
    final int CAPTCHA_TYPE_GIF = 20;
    /*
     验证码类型: 中文
     */
    final int CAPTCHA_TYPE_CHINESE = 30;
    /*
     验证码类型: 动态中文
     */
    final int CAPTCHA_TYPE_GIF_CHINESE = 40;
    /*
     验证码类型: 算术
     */
    final int CAPTCHA_TYPE_ARITHMETIC = 50;

    /*
     验证码缓存key前缀
     */
    String CAPTCHA_KEY_PRE = Constants.REDIS_PRE + "captcha:";
    /*
     验证码图片键名
     */
    String IMAGE = "image";
    /*
     验证码UUID键名
     */
    String UUID = "uuid";
    /*
     验证码缓存时间,分
     */
    Integer CAPTCHA_CACHE_TIME = 3;

    /**
     * 生成普通图片验证码
     *
     * @param type 验证码类型
     * @return Map 验证码值,和base64转换的图片
     */
    Map<String, String> outputCaptchaImg(Integer type) throws IOException;

    /**
     * 校验图片验证码
     *
     * @param uuid        存入redis的key
     * @param captchaCode 验证码
     * @throws RuntimeException 验证失败抛出错误
     */
    void verifyCaptchaCode(String uuid, String captchaCode) throws RuntimeException;
}

服务实现类:

package com.dragonwu.service.impl;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.dragonwu.service.EasyCaptchaService;
import com.dragonwu.utils.RedisUtil;
import com.dragonwu.utils.SnowflakeIdGen;
import com.wf.captcha.ArithmeticCaptcha;
import com.wf.captcha.ChineseCaptcha;
import com.wf.captcha.ChineseGifCaptcha;
import com.wf.captcha.GifCaptcha;
import com.wf.captcha.SpecCaptcha;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Service
public class EasyCaptchaServiceImpl implements EasyCaptchaService {
    /*
    redis工具
     */
    @Autowired
    private RedisUtil redisUtil;

    /*
    雪花算法生成唯一key
     */
    @Autowired
    private SnowflakeIdGen snowflakeIdGen;

    @Override
    public Map<String, String> outputCaptchaImg(Integer type) throws IOException {
        String codeValue = "";
        Map<String, String> map = new HashMap<>();
        switch (type) {
            case CAPTCHA_TYPE_DEFAULT_PNG:
                SpecCaptcha specCaptcha = new SpecCaptcha(CAPTCHA_WIDTH, CAPTCHA_HEIGHT, CAPTCHA_LENGTH);
                codeValue = specCaptcha.text().toLowerCase();
                map.put(EasyCaptchaService.IMAGE, specCaptcha.toBase64());
                break;
            case CAPTCHA_TYPE_GIF:
                GifCaptcha gifCaptcha = new GifCaptcha(CAPTCHA_WIDTH, CAPTCHA_HEIGHT, CAPTCHA_LENGTH);
                codeValue = gifCaptcha.text().toLowerCase();
                map.put(EasyCaptchaService.IMAGE, gifCaptcha.toBase64());
                break;
            case CAPTCHA_TYPE_CHINESE:
                ChineseCaptcha chineseCaptcha = new ChineseCaptcha(CAPTCHA_WIDTH, CAPTCHA_HEIGHT, CAPTCHA_LENGTH);
                codeValue = chineseCaptcha.text();
                map.put(EasyCaptchaService.IMAGE, chineseCaptcha.toBase64());
                break;
            case CAPTCHA_TYPE_GIF_CHINESE:
                ChineseGifCaptcha chineseGifCaptcha = new ChineseGifCaptcha(CAPTCHA_WIDTH, CAPTCHA_HEIGHT, CAPTCHA_LENGTH);
                codeValue = chineseGifCaptcha.text();
                map.put(EasyCaptchaService.IMAGE, chineseGifCaptcha.toBase64());
                break;
            case CAPTCHA_TYPE_ARITHMETIC:
                ArithmeticCaptcha arithmeticCaptcha = new ArithmeticCaptcha(CAPTCHA_WIDTH, CAPTCHA_HEIGHT);
                codeValue = arithmeticCaptcha.text();
                map.put(EasyCaptchaService.IMAGE, arithmeticCaptcha.toBase64());
                break;
            default:
                throw new IOException("类型不存在");
        }
        //生成redis键名
        String uuid = String.valueOf(snowflakeIdGen.nextId());
        //存入redis
        redisUtil.setObject(EasyCaptchaService.CAPTCHA_KEY_PRE + uuid, codeValue, EasyCaptchaService.CAPTCHA_CACHE_TIME, TimeUnit.MINUTES);
        map.put(EasyCaptchaService.UUID, uuid);
        return map;
    }

    @Override
    public void verifyCaptchaCode(String uuid, String captchaCode) throws RuntimeException {
        String cacheCaptchaCode = (String) redisUtil.getObject(EasyCaptchaService.CAPTCHA_KEY_PRE + uuid);
        if (StringUtils.isBlank(cacheCaptchaCode)) {
            throw new RuntimeException("验证码已过期,请重新获取");
        }
        if (!cacheCaptchaCode.equals(captchaCode.toLowerCase())) {
            throw new RuntimeException("验证码错误");
        }
    }
}

9、雪花算法生成唯一id

package com.dragonwu.utils;

import org.springframework.stereotype.Component;

/**
 * 采用twitter的雪花算法,生成有一定顺序且不重复的id,结果类型为64位的long型
 */
@Component
public class SnowflakeIdGen {
    //集群id
    private final long dataCenterId;
    //机器id
    private final long workerId;
    //集群id的bit位数
    private final long dataCenterIdBits = 5L;
    //机器id的bit位数
    private final long workerIdBits = 5L;
    //序列号
    private long sequenceId;
    //上一次生成id使用的timestamp ,以毫秒为单位
    private long lastTimestamp = 1L;

    /**
     * 若没有指定集群id和机器id,则默认均为0
     */
    public SnowflakeIdGen() {
        this(0, 0);
    }

    /**
     * 指定集群id和机器id
     */
    public SnowflakeIdGen(long dataCenterId, long workerId) {
        //集群id的最大编号
        long maxdataCenterId = ~(-1L << dataCenterIdBits);
        if (dataCenterId < 0 || dataCenterId > maxdataCenterId) {
            throw new RuntimeException(String.format("dataCenterId greater than %d or less than 0", maxdataCenterId));
        }
        //机器id的最大编号
        long maxWorkerId = ~(-1L << workerIdBits);
        if (workerId < 0 || workerId > maxWorkerId) {
            throw new RuntimeException(String.format("workerId greater than %d or less than 0", maxWorkerId));
        }
        this.dataCenterId = dataCenterId;
        this.workerId = workerId;
    }

    /**
     * 生成全局唯一的id
     */
    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();
        if (timestamp < lastTimestamp) {  //出现这种情况,通常是由于机器时间出问题了
            throw new RuntimeException("machine time error");
        }

        //同一时刻生成的id号
        //序列号的bit位数
        long sequenceIdBits = 12L;
        if (timestamp == lastTimestamp) {
            //序列号的掩码
            long sequenceIdMask = ~(-1L << sequenceIdBits);
            sequenceId = (sequenceId + 1) & sequenceIdMask;
            if (sequenceId == 0) {  //说明当前毫秒的序列号用完了,需从下个毫秒数开始重新计数
                timestamp = nextTimestamp(lastTimestamp);
            }
        } else {
            //否则序列号从0开始
            sequenceId = 0L;
        }

        lastTimestamp = timestamp;
        //生成最终结果时,集群id需移动的bit位数
        long timestampShiftBits = sequenceIdBits + workerIdBits + dataCenterIdBits;
        //生成最终结果时,集群id需移动的bit位数
        long dataCenterIdShiftBits = sequenceIdBits + workerIdBits;
        //生成最终结果时,机器id需移动的bit位数
        //去掉过去的时间,即从指定时间(本例以2017-10-12 00:00:00)开始算,
        // 大约可用69.5年(41位的时间位,最大值换成毫秒,再换算成年,大约69.5年)
        //1507737600000为从1970-01-01 00:00:00到2017-10-12 00:00:00经过的毫秒数
        long pastMills = 1507737600000L;
        return ((timestamp - pastMills) << timestampShiftBits)
                | (dataCenterId << dataCenterIdShiftBits)
                | (workerId << sequenceIdBits)
                | sequenceId;
    }

    /**
     * 获取上次取数毫秒的下一时刻
     */
    long nextTimestamp(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

10、web渲染工具类

package com.dragonwu.utils;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * web渲染工具类
 *
 * @author DragonWu
 * @since 2022-10-07 15:36
 **/
public class WebUtil {

    /**
     * @param response   渲染对象
     * @param jsonString 待渲染的字符串
     */
    public static void renderString(HttpServletResponse response, String jsonString) {
        try {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().write(jsonString);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

二、SpringSecurity的配置

依赖引入:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

1、SpringSecurity配置类

package com.dragonwu.config;

import com.dragonwu.filter.TokenAuthenticationFilter;
import com.dragonwu.handler.AccessDeniedHandlerImpl;
import com.dragonwu.handler.AuthenticationEntryPointImpl;
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.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;
import java.util.Collections;

/**
 * @author DragonWu
 * @since 2022-10-06 21:14
 **/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)  //开启权限访问注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //允许匿名访问的
    private static final String[] ALLOW_ASK = {
            "/login",
            "/captcha"
    };
    //总数允许访问的
    private static final String[] ALWAYS_ALLOW_ASK = {
            "/v2/api-docs",
            "/swagger-resources/configuration/ui",//用来获取支持的动作
            "/swagger-resources",//用来获取api-docs的URI
            "/swagger-resources/configuration/security",//安全选项
            "/webjars/**",
            "/swagger-ui.html",//以上为api文档接口访问路径
            "/static/**",//放行静态资源
            "**/open" //开放访问的资源
    };

    //自定义登录
    @Autowired
    private UserDetailsService userDetailsService;

    //token过滤器
    @Autowired
    private TokenAuthenticationFilter tokenAuthenticationFilter;

    //禁止访问异常处理器
    @Autowired
    private AccessDeniedHandlerImpl accessDeniedHandler;

    //认证异常处理器
    @Autowired
    private AuthenticationEntryPointImpl authenticationEntryPoint;

    /*
    密码加密器
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /*
     设置自定义登录逻辑
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
    }

    /*
    自定义登录,用户认证
     */
    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    /*
    页面资源授权
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                //对应登录接口允许匿名访问
                .antMatchers(ALWAYS_ALLOW_ASK).permitAll()
                .antMatchers(ALLOW_ASK).anonymous()
                //除上面接口全都需要鉴权访问
                .anyRequest().authenticated();

        //添加过滤器
        http.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        //添加异常处理器
        http.exceptionHandling()
                //认证失败的处理器
                .authenticationEntryPoint(authenticationEntryPoint)
                //授权失败处理器
                .accessDeniedHandler(accessDeniedHandler);

        //跨域配置
        http.cors()
                .configurationSource(corsConfigurationSource());

    }

    /*
     跨域配置对象
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        //配置允许访问的服务器域名
        configuration.setAllowedOrigins(Collections.singletonList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(Collections.singletonList("*"));
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

2、SpringSecurity异常处理器

认证异常处理器:

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        R<String> result = R.restResult(null, HttpStatus.UNAUTHORIZED.value(), "认证失败");
        String json = JSON.toJSONString(result);
        //处理异常,返回结果
        WebUtil.renderString(httpServletResponse, json);
    }
}

禁止访问异常处理器:

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        R<String> result = R.restResult(null, HttpStatus.FORBIDDEN.value(), "禁止访问");
        String json = JSON.toJSONString(result);
        //处理异常,返回结果
        WebUtil.renderString(httpServletResponse, json);
    }
}

异常处理器可以将异常自定义处理。

三、JWT+SpringSecurity实现登录的主要步骤

1、登录图解

2、登录开始前首先创建UserDetails实体类的子类

package com.dragonwu.model;

import com.alibaba.fastjson.annotation.JSONField;
import com.dragonwu.model.entity.SysUser;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.tomcat.util.buf.StringUtils;
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.Objects;
import java.util.stream.Collectors;

/**
 * @author DragonWu
 * @since 2022-10-06 20:29
 **/
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private SysUser sysUser;

    //用户角色权限
    private List<String> jurisdiction;

    //将不会被序列化到redis里
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;

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

    public String getJurisdictionToString() {
        return StringUtils.join(jurisdiction, ',');
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //把jurisdiction里的权限信息字符串封装成SimpleGrantedAuthority对象
        if (!Objects.isNull(authorities)) {
            return authorities;
        }
        authorities = jurisdiction.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }

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

    @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、创建UserDetailsService的实现类

通过UserDetailsService的实现可对用户对象进行自定义返回

package com.dragonwu.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.dragonwu.model.entity.SysUser;
import com.dragonwu.mapper.SysUserMapper;
import com.dragonwu.model.LoginUser;
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.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
 * @author DragonWu
 * @since 2022-10-06 20:47
 **/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询用户信息
        SysUser user = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName,username));
        //如果没有查询到用户就抛出异常
        if (Objects.isNull(user)) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
        //查询对应的权限信息和角色信息
        // 这一般需要查询role表和jurisdiction表的权限,这里略过
        List<String> jurisdiction=new ArrayList<>(Arrays.asList("admin","normal","ROLE_manager"));
        //将数据封装到UserDetail返回
        return new LoginUser(user, jurisdiction);
    }
}

4、登录的实现类与接口

接口:

package com.dragonwu.controller;

import com.alibaba.fastjson.JSONObject;
import com.dragonwu.constant.Constants;
import com.dragonwu.model.R;
import com.dragonwu.model.param.LoginParam;
import com.dragonwu.model.vo.LoginVo;
import com.dragonwu.service.LoginService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@Slf4j
public class LoginController {

    @Autowired
    private LoginService loginService;

    @PostMapping("/login")
    public R<LoginVo> login(@RequestBody LoginParam loginParam) {
        try {
            return R.ok(loginService.login(loginParam), Constants.LOGIN_OK);
        } catch (RuntimeException msg) {
            //返回失败结果
            return R.fail(null, msg.getMessage());
        } catch (Exception e) {
            log.error("后台登录出现异常:" + JSONObject.toJSONString(loginParam), e);
            return R.fail(null, Constants.LOGIN_FAIL);
        }
    }

    @GetMapping("/logout.do")
    public R<String> logout() {
        try {
            loginService.logout();
            return R.ok(null, Constants.LOGOUT_OK);
        } catch (RuntimeException msg) {
            return R.fail(null, msg.getMessage());
        } catch (Exception e) {
            log.error("退出登录出现异常:" + e);
            return R.fail(null, Constants.LOGOUT_FAIL);
        }
    }

    @GetMapping("/captcha")
    public R<Map<String, String>> captcha() {
        try {
            return R.ok(loginService.getCaptcha(), Constants.GET_RESOURCE_OK);
        } catch (RuntimeException msg) {
            return R.fail(null, msg.getMessage());
        } catch (Exception e) {
            log.error("获取验证码出现异常:", e);
            return R.fail(null, Constants.GET_RESOURCE_FAIL);
        }
    }
}

服务层接口:

public interface LoginService {

    /**
     * 用户登录
     * @param loginParam 登录参数
     * @return 登录返回对象
     */
    LoginVo login(LoginParam loginParam);

    /**
     * 退出登录
     */
    void logout() throws RuntimeException;

    /**
     * 获取验证码
     * @return Map UUID验证码对应redis里的key,image的base64编码
     */
    Map<String,String> getCaptcha() throws IOException;

}

服务层实现类:

package com.dragonwu.service.impl;

import com.dragonwu.constant.Constants;
import com.dragonwu.model.LoginUser;
import com.dragonwu.model.OnlineUser;
import com.dragonwu.model.param.LoginParam;
import com.dragonwu.model.vo.LoginVo;
import com.dragonwu.service.EasyCaptchaService;
import com.dragonwu.service.LoginService;
import com.dragonwu.utils.JwtUtil;
import com.dragonwu.utils.RedisUtil;
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.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @author DragonWu
 * @since 2022-10-07 19:02
 **/
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private EasyCaptchaService easyCaptchaService;


    @Override
    public LoginVo login(LoginParam loginParam) throws RuntimeException {
        //判断验证码是否正确
        easyCaptchaService.verifyCaptchaCode(loginParam.getUuid(), loginParam.getCaptcha());

        //AuthenticationManager authenticate进行用户认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginParam.getUsername(), loginParam.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //如果认证没通过给出相应的提示
        if (Objects.isNull(authenticate)) {
            throw new RuntimeException(Constants.LOGIN_ERROR);
        }

        //如果认证通过了,获取UserDetailService返回过期的UserDetail对象
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        //使用primaryKey生成JWT
        String primaryKey = loginUser.getSysUser().getUserId().toString();
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtUtil.JWT_PRIMARY_KEY, primaryKey);
        String token = JwtUtil.createJwt(claims, JwtUtil.DEFAULT_EXPIRE_MINUTES);

        //把用户信息存入redis
        OnlineUser onlineUser = new OnlineUser(loginUser.getSysUser(), loginUser.getJurisdictionToString());
        redisUtil.setObject(Constants.ONLINE_PRE + primaryKey, onlineUser, JwtUtil.DEFAULT_EXPIRE_MINUTES, TimeUnit.MINUTES);

        //将返回结果封装达到返回对象
        return new LoginVo(loginUser.getSysUser(), loginUser.getJurisdiction(), token);
    }

    @Override
    public void logout() throws RuntimeException {
        try {
            //获取SecurityContextHolder里的用户id
            UsernamePasswordAuthenticationToken authentication =
                    (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
            OnlineUser onlineUser = (OnlineUser) authentication.getPrincipal();
            String primaryKey = String.valueOf(onlineUser.getSysUser().getUserId());
            //删除redis中的值
            redisUtil.deleteObject(Constants.ONLINE_PRE + primaryKey);
        } catch (Exception e) {
            throw new RuntimeException(Constants.LOGOUT_FAIL);
        }
    }

    @Override
    public Map<String, String> getCaptcha() throws IOException {
        return easyCaptchaService.outputCaptchaImg(EasyCaptchaService.CAPTCHA_TYPE_GIF);
    }
}

这里我们便实现了登录逻辑。

5、认证过滤器

判断用户是否已登录

package com.dragonwu.filter;

import com.alibaba.fastjson.JSON;
import com.dragonwu.constant.Constants;
import com.dragonwu.model.OnlineUser;
import com.dragonwu.model.R;
import com.dragonwu.utils.JwtUtil;
import com.dragonwu.utils.RedisUtil;
import com.dragonwu.utils.WebUtil;
import org.junit.platform.commons.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;

/**
 * token验证过滤器
 *
 * @author DragonWu
 * @since 2022-10-07 19:02
 **/
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    public static final String HEADER = "Authorization";
    public static final String PARAM = "token";

    @Autowired
    private RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //获得前端请求中的token
        String token = request.getHeader(HEADER);
        if (StringUtils.isBlank(token)) {
            token = request.getParameter(PARAM);
        }
        //如果token为空,放行,验证失败
        if (StringUtils.isBlank(token)) {
            chain.doFilter(request, response);
            return;
        }

        //解析token
        Map<String, Object> claims = JwtUtil.parseJwt(token);
        if (Objects.isNull(claims)) {//解析异常
            R<String> result = R.restResult(null, HttpStatus.FORBIDDEN.value(), "token非法");
            String json = JSON.toJSONString(result);
            //返回解析错误的json数据
            WebUtil.renderString(response, json);
            return;
        }

        //获取用户主键,以便到redis中查询
        String primaryKey = (String) claims.get(JwtUtil.JWT_PRIMARY_KEY);
        //从redis中获取token对应的数据
        String redisKey = Constants.ONLINE_PRE + primaryKey;
        //获取用户信息
        OnlineUser onlineUser = (OnlineUser) redisUtil.getObject(redisKey);
        if (Objects.isNull(onlineUser)) {
            R<String> result = R.restResult(null, HttpStatus.UNAUTHORIZED.value(), "认证失败");
            String json = JSON.toJSONString(result);
            //返回请求失败的json数据
            WebUtil.renderString(response, json);
            return;
        }

        //存入SecurityContextHolder中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(onlineUser, null,
                        AuthorityUtils.commaSeparatedStringToAuthorityList(onlineUser.getAuthorities()));
        //获取权限信息封装到authenticationToken里
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

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

    }
}

四、测试

1、登录测试

首先访问验证码接口获取验证码的图片和uuid:

 

 服务器通过用户输入的验证码结果和redis中的缓存进行比较得出是否验证码正确。

访问登录接口:

 登录成功:

 2、资源访问测试

未携带JWT:

 携带token就是登录的状态:

携带错误的token测试:可以看到错误的token会被服务器警告。

 

 3、退出登录

退出登录后,redis中缓存的用户信息将被删除。

 五、案例源码链接

SpringCloudAlibaba各技术栈实现的案例代码集 我的笔记: SpringCloudAlibaba各技术栈实现的案例代码集 我的笔记 - Gitee.com

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值