SpringSecurity--安全登录实现

98 阅读15分钟

继上篇

对于安全系数要求高的登录系统

通常会采用以下措施:

注意:jwt值可以看作一个token,下面会有两对公钥私钥,分别为public_key,private_keypublic_sign_key,private_sign_key

  1. 前端的账号密码验证码传输时,使用后端提供的公钥加密(public_key)也可以使用对称加密,即前后台使用一个密钥),并生成一个uuid(uuid需存在前端缓存sessionStorage里)方便后续使用
  2. 后端接收uuid后,将uuid作为key的一部分,jwt的值私钥签名(private_sign_key)之后作为value值,后续后端存入redis中。
  3. 先进行私钥解密(private_key)就能获取前端的账号密码验证码信息,后端做登录密码验证时,需要将私钥解密的数据(即前端传过来的密码)再次加密(密码加密,一般可自行MD5+盐加密或者使用PasswordEncoder内置的实现方法),再和数据库里的密码(存的加密后的数据)比对,一致则登录成功,否则登录失败。
  4. 前端调用后台接口前都需要验签(白名单接口除外),此时需要将sessionStorage中的uuid和参数的其他的参数一并传到后台接口,后台接口获取请求参数中的uuid,并去查找存在redis中的jwt值签名值,并用公钥解签(public_sign_key)这个值(验证jwt是否是系统颁发的)
  5. 验签通过之后即可调用接口的信息

代码实现

前端代码(vue)

jsencrypt.js 用户密码验证码等信息加密

import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'

// 密钥对生成 http://web.chacuo.net/netrsakeypair
const publicKey = '你的数据传输公钥'

// 账号密码传输加密
export function encrypt(txt) {
  const encryptor = new JSEncrypt()
  encryptor.setPublicKey(publicKey) // 设置公钥
  return encryptor.encrypt(txt) // 对数据进行加密
}

login.vue 登录页面

<template>
  <div class="center">
    <el-form :rules="rules" :model="state.form" label-width="100px" style="max-width: 360px">
      <el-form-item prop="username" label="账号">
        <el-input type="text" id="username" v-model="state.form.username" required/>
      </el-form-item>
      <el-form-item prop="password" label="密码">
        <el-input type="password" id="password" v-model="state.form.password" required/>
      </el-form-item>
      <el-form-item prop="captcha" label="验证码">
        <el-row justify="space-between">
          <el-col :span="12">
            <el-input v-model="state.form.captcha" class="custom-input-login" placeholder="请输入验证码"></el-input>
          </el-col>
          <el-col :span="11">
            <Captcha ref="captchaRef" />
          </el-col>
        </el-row>
      </el-form-item>
      <el-button type="primary" plain @click="login">登录</el-button>
      <p v-if="state.error != null" class="error">{{ state.error }}</p>
    </el-form>
  </div>
</template>
<script>
import request from "@/util/request";
import { reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { encrypt } from "@/util/jsencrypt";
import Captcha from "@/components/Captcha.vue";
export default {
  // 引入组件
  components: {
    Captcha,
  },
  // 引入路由
  setup() {
    const userRouter = useRouter();
    // 引入loading
    const state = reactive({
      form: {
        username: "",
        password: "",
        captcha: "", // 验证码
        uuid: "", // uuid
      },
      error: "",
    });

    // 表单验证
    const rules = {
      username: [
        { required: true, message: "请输入账号", trigger: "blur" },
        {
          min: 5,
          max: 15,
          message: "账号长度为 5 到 15 个字符",
          trigger: "blur",
        },
      ],
      password: [
        { required: true, message: "请输入密码", trigger: "blur" },
        {
          min: 6,
          max: 20,
          message: "密码长度为 6 到 20 个字符",
          trigger: "blur",
        },
      ],
      captcha: [{ required: true, message: "请输入验证码", trigger: "blur" }],
    };

    // 登录
    const login = () => {
      try {
        let uuid = generateUUID();
        // 将账号密码单独加密,也可以一起加密,看个人选择
        const formData = {
          username: encrypt(state.form.username),
          password: encrypt(state.form.password),
          captcha: state.form.captcha,
          uuid: uuid,
        };
        request.post("/login", formData).then((res) => {
          if (res.data != null) {
            sessionStorage.setItem("uuid", uuid);//缓存uuid信息后续需要uuid+用户去拿token信息
            userRouter.push("/index"); //登录成功之后进行页面的跳转,跳转到主页
          } else {
            state.error = "登录失败";
            captchaRef.value.refreshCaptcha(); // 刷新验证码
          }
        });
      } catch (error) {
        state.error = "登录失败";
        captchaRef.value.refreshCaptcha(); // 刷新验证码
      }
    };
    // 生成uuid
    const generateUUID = () => {
      return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
        /[xy]/g,
        function (c) {
          const r = (Math.random() * 16) | 0,
            v = c === "x" ? r : (r & 0x3) | 0x8;
          return v.toString(16);
        }
      );
    };
    // 引入验证码
    const captchaRef = ref(null);
    return {
      state,
      login,
      loginWithWechat,
      captchaRef,
      rules,
      generateUUID,
    };
  },
};
</script>
<style>
    .center {
      display: flex;
      justify-content: center; /* 水平居中 */
      align-items: center; /* 垂直居中 */
      width: 100%;
      height: 100%;
    }

    .error {
      color: rgb(201, 43, 43);
      font-size: 30px;
    }
</style>

Captcha.vue 验证码刷新组件

<template>
  <div class="captcha">
    <img :src="captchaSrc" @click="refreshCaptcha" alt="验证码" />
  </div>
</template>

<script setup>
import { ref, onMounted } from "vue";
import request from "@/util/request";

// 定义一个变量来存储验证码的 base64 字符串
const captchaSrc = ref("");
// 接收父组件传递的 uuid
const emit = defineEmits(["uuidReceived"]);

// 刷新验证码
const refreshCaptcha = async () => {
  request.post("/login/getCaptcha").then((res) => {
    if (res && res.data && res.data.captcha) {
      captchaSrc.value = `data:image/png;base64,${res.data.captcha}`;
      if (res.data.uuid) {
        // 向父组件传递 uuid
        emit("uuidReceived", res.data.uuid);
      }
    }
  });
};

// 组件挂载时刷新验证码
onMounted(() => {
  refreshCaptcha();
});

// 向外暴露刷新验证码的方法
defineExpose({
  refreshCaptcha,
});
</script>
 
<style scoped>
    .captcha img {
      cursor: pointer;
      border: 1px solid #dcdcdc;
      border-radius: 4px;
    }
</style>

request.js

import axios from 'axios'

const request = axios.create({
    baseURL: '/knowledge',  // 注意!! 这里是全局统一加上了 '/knowledge' 前缀,也就是说所有接口都会加上'/knowledge'前缀在,页面里面写接口的时候就不要加 '/knowledge'了,否则会出现两个'/knowledge',类似 '/knowledge/knowledge/user'这样的报错,切记!!!
    timeout: 5000 // 请求超时时间
})

// request 拦截器
// 可以自请求发送前对请求做一些处理
// 比如统一加token,对请求参数统一加密
request.interceptors.request.use(config => {
    config.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';
    // 将uuid传到后端,方便后续验签
    config.headers['uuid'] = sessionStorage.getItem("uuid");  // 设置请求头
    return config
}, error => {
    return Promise.reject(error)
});

// response 拦截器
// 可以在接口响应后统一处理结果
request.interceptors.response.use(
    response => {
        let res = response.data;
        // 如果是返回的文件
        if (response.config.responseType === 'blob') {
            return res
        }
        // 兼容服务端返回的字符串数据
        if (typeof res === 'string') {
            res = res ? JSON.parse(res) : res
        }
        return res;
    },
    error => {
        console.log('err' + error) // for debug
        return Promise.reject(error)
    }
)

export default request

后端代码(java)

/**
 * SpringSecurity身份认证提供者(这是自定义的,方便加自定义的校验等)
 */
@Component
public class SpringSecurityAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    /**
     * 用户身份认证服务接口
     */
    @Autowired
    private IUserAuthenticationService userAuthenticationService;

    /**
     * 密码加密器
     */
    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 密码加密  使用内置的BCryptPasswordEncoder加密,也可自定义实现加密
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 根据用户名或其他凭证来查找用户,并返回一个包含用户详细信息的 UserDetails 对象
     *
     * @param username                            用户名
     * @param usernamePasswordAuthenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
        try {
            // 解密账号
            username = RSAUtils.privateDecrypt(username, RSAUtils.getPrivateKey(SecretConstants.DATA_RSA_PRIVATE_KEY));
            UserDetails loadedUser = this.userAuthenticationService.loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        } catch (UsernameNotFoundException | InternalAuthenticationServiceException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

    /**
     * 用于在用户密码验证之后执行额外的身份验证检查。
     * 一旦用户的密码验证成功,Spring Security 将调用这个方法,以便执行自定义的额外检查。
     * 这些额外的检查可以包括检查用户是否被锁定、密码是否已过期、账户是否被禁用等。
     * 这个方法允许你在完成基本的密码验证后,添加任何自定义的身份验证逻辑
     *
     * @param userDetails 后端查出来的用户对象(账号&&密码)
     * @param userToken   前端传过来的用户对象(账号&&密码)
     * @throws AuthenticationException
     */
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken userToken) throws AuthenticationException {
        // 账号
        Object principal = userToken.getPrincipal();
        // 密码
        Object credentials = userToken.getCredentials();
        if (principal == null || credentials == null) {
            throw new BadCredentialsException("账号或密码不能为空!");
        }
        String username;
        String password;
        try {
            // 解密账号
            username = RSAUtils.privateDecrypt(String.valueOf(principal), RSAUtils.getPrivateKey(SecretConstants.DATA_RSA_PRIVATE_KEY));
            // 解密密码
            password = RSAUtils.privateDecrypt(String.valueOf(credentials), RSAUtils.getPrivateKey(SecretConstants.DATA_RSA_PRIVATE_KEY));
        } catch (Exception e) {
            throw new BadCredentialsException("账号或密码解密错误!");
        }
        // 拿到后端查询到的账号密码信息进行比较
        if (!StringUtils.equals(username, userDetails.getUsername())) {
            throw new BadCredentialsException("账号或密码错误!");
        }
        /**
         * 比较用户输入的密码加密之后的值与数据库里存的是否一致
         * 密码加密比对过程:
         *      数据库密码解析获取其中的 随机盐 的值,然后将这个盐和用户输入的密码通过同样的加密方式加密
         *      然后比对这个密码和数据库的加密值是否一致
         */
        if (!passwordEncoder.matches(password, userDetails.getPassword())) {
            throw new BadCredentialsException("账号或密码错误!");
        }
    }
}

SpringSecurity核心类 SpringSecurityConfig

@Configuration
@EnableWebSecurity(debug = true)
@Slf4j
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * SpringSecurity身份认证提供者(自定义的身份认证类)
     */
    @Resource
    private SpringSecurityAuthenticationProvider securityAuthenticationProvider;

    /**
     * JWT配置属性(方便获取yml文件中的值,用来接收有关于jwt的属性)
     */
    @Resource
    private JwtProperties jwtProperties;

    /**
     * JWT的工具类(里面包含了签名和验签等)
     */
    @Autowired
    private JwtTokenUtils jwtTokenUtils;

    /**
     * redis操作组件
     */
    @Resource
    private RedisComponent redisComponent;

    /**
     * 登录时忽略URL,即以这个开头的不需要登录验证即可访问
     */
    protected static final String[] PUBLC_URLS = {
            "/login",
            "/user/login",
            "/login/getCaptcha",
            "/register"
    };

    /**
     * 用于配置认证管理器,包括用户认证、角色授权等。您可以在此方法中配置内存用户、数据库用户、LDAP 用户等
     * 身份验证管理器生成器
     * this.securityAuthenticationProvider就是自定义的那个类
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(this.securityAuthenticationProvider);
    }

    /**
     * 用于配置 Web 安全设置,包括忽略某些 URL 的过滤器链、静态资源的安全配置等
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        // 忽略对以 "/public/" 开头的静态资源的访问控制,这样用户可以在不需要进行身份验证的情况下访问这些静态资源。
        web.ignoring().mvcMatchers("/public/**").requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

    /**
     * 定义登陆成功返回信息
     */
    private class AjaxAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
        @SneakyThrows
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
            // 接收前端请求的请求头中的uuid用于后续操作 
            String uuid = request.getParameter("uuid");
            // 组装JWT
            SpringSecurityActiveUser securityActiveUser = (SpringSecurityActiveUser) authentication.getPrincipal();
            SecurityContextHolder.getContext().setAuthentication(authentication);
            // 生成访问令牌
            String accessToken = jwtTokenUtils.createAccessToken(uuid, securityActiveUser.getUsername());
            // 生成刷新令牌,如果accessToken令牌失效,则使用refreshToken重新获取令牌(refreshToken过期时间必须大于accessToken)
            String refreshToken = jwtTokenUtils.creatRefreshToken(accessToken);
            // 存入Redis
            redisComponent.setObj(RedisConstants.ACCESS_TOKEN_KEY + "_" + uuid, accessToken, jwtProperties.getAccessTokenExpiration(), TimeUnit.SECONDS);
            redisComponent.setObj(RedisConstants.REFRESH_TOKEN_KEY + "_" + uuid, refreshToken, jwtProperties.getRefreshTokenExpiration(), TimeUnit.SECONDS);
            redisComponent.setObj(RedisConstants.ACTIVE_USER_KEY + "_" + uuid, JSON.toJSONString(securityActiveUser, SerializerFeature.WriteMapNullValue), jwtProperties.getRefreshTokenExpiration(), TimeUnit.SECONDS);
            // 设置返回值
            LoginTokenResponse result = new LoginTokenResponse();
            Result<?> success = Result.success(result);
            Result.responseJson(success, response);
        }
    }

    /**
     * 用于配置 HTTP 安全设置,包括 URL 访问权限、表单登录、注销、CSRF 保护等
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 配置授权规则
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests
                                .antMatchers(PUBLC_URLS).permitAll()
                                // 静态资源,可匿名访问
                                .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
                                .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
                                // 除上面外的所有请求全部需要鉴权认证
                                .anyRequest().authenticated()
                )
                // 配置表单登录
                .formLogin(formLogin ->
                                formLogin.loginPage("/login")
                                        .usernameParameter("username")
                                        .passwordParameter("password")
                                        .successForwardUrl("/index")
                                        .successHandler(new AjaxAuthSuccessHandler())
                                        .failureHandler(new AjaxAuthFailHandler()).permitAll()
                )
                // 配置登出
                .logout(logout ->
                        logout.logoutUrl("/logout").logoutSuccessHandler(new AjaxLogoutSuccessHandler())
                )
                // CSRF禁用,因为不使用session
                .csrf(csrf ->
                        csrf.disable()
                )
                //禁用session,JWT校验不需要session,需要禁用它
                .sessionManagement(sessionManage ->
                        sessionManage.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 配置异常处理
                .exceptionHandling(exceptionHandling ->
                        exceptionHandling.accessDeniedPage("/error/403")
                )
                // 配置HTTP标头安全性
                .headers(headers ->
                        headers.frameOptions().sameOrigin().contentSecurityPolicy("frame-ancestors 'self'")
                )
                // 配置会话管理
                .sessionManagement(sessionManagement ->
                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                )
                // 配置请求缓存
                .requestCache(requestCache ->
                        requestCache.requestCache(new HttpSessionRequestCache())
                )
                // 配置匿名用户的安全性
                .anonymous(anonymous ->
                        anonymous.authorities("ROLE_ANONYMOUS")
                )
                // 配置HTTP Basic认证(支持基本认证,因为 OAuth2 认证往往用于不同种类客户端,所以基本认证支持是必要的)
                .httpBasic(httpBasic -> {
                    httpBasic.realmName("My Realm");
                })
                // 配置Remember-Me功能
                .rememberMe(rememberMe -> {
                    rememberMe.key("uniqueAndSecret");
                })
                // 配置跨域资源共享(CORS)
                .cors(cors -> {
                    cors.configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues());
                })
                // 添加自定义过滤器,在登录验证之前执行
                .addFilterBefore(new TokenAuthenticationFilter(this.jwtProperties, this.jwtTokenUtils, this.redisComponent), UsernamePasswordAuthenticationFilter.class);
    }
}
/**
 * 校验token的过滤器,直接获取header中的token进行校验
 *
 * @author mjr
 * @date 2024/12/11 17:22
 **/
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    private final AntPathMatcher pathMatcher = new AntPathMatcher();

    /**
     * JWT配置属性
     */
    private JwtProperties jwtProperties;

    /**
     * JWT的工具类
     */
    private JwtTokenUtils jwtTokenUtils;

    /**
     * redis操作组件
     */
    private RedisComponent redisComponent;

    public TokenAuthenticationFilter(JwtProperties jwtProperties, JwtTokenUtils jwtTokenUtils, RedisComponent redisComponent) {
        this.jwtProperties = jwtProperties;
        this.jwtTokenUtils = jwtTokenUtils;
        this.redisComponent = redisComponent;
    }

    /**
     * token存在则校验token
     * 1. token是否存在
     * 2. token存在:
     * 2.1 校验token中的用户名是否失效
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 不需要认证的接口,直接放行
        String uri = StringUtils.removeStart(request.getRequestURI(), "/knowledge-base");
        // 检查请求的URI是否匹配任何一个模式
        for (String pattern : this.jwtProperties.getAntMatchers()) {
            if (this.pathMatcher.match(pattern, uri)) {
                // 继续执行下一个过滤器
                filterChain.doFilter(request, response);
                return;
            }
        }
        String uuid = request.getHeader("uuid");
        if (!StringUtils.isEmpty(uuid)) {
            // SecurityContextHolder.getContext().getAuthentication()==null 未认证则为true
            if (!StringUtils.isEmpty(uuid) && SecurityContextHolder.getContext().getAuthentication() == null) {
                // 在线用户
                Object securityActiveUserObj = this.redisComponent.getObj(RedisConstants.ACTIVE_USER_KEY + "_" + uuid);
                String securityActiveUserJsonStr = securityActiveUserObj != null ? String.valueOf(securityActiveUserObj) : null;
                SpringSecurityActiveUser securityActiveUser = securityActiveUserJsonStr != null ? JSON.parseObject(securityActiveUserJsonStr, SpringSecurityActiveUser.class) : null;
                // Redis中的accessToken
                Object accessTokenObj = this.redisComponent.getObj(RedisConstants.ACCESS_TOKEN_KEY + "_" + uuid);
                String accessToken = accessTokenObj != null ? String.valueOf(accessTokenObj) : null;
                if (accessToken != null && !"".equals(accessToken)) {
                    // 将用户信息存入 authentication,方便后续校验
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(securityActiveUser,
                            securityActiveUser.getPassword(),
                            securityActiveUser.getAuthorities());
                    usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    // 将 authentication 存入 ThreadLocal,方便后续获取用户信息
                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                }
            }
        }
        // 继续执行下一个过滤器
        filterChain.doFilter(request, response);
    }
}
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
……其他的导包自行导入
/**
 * 自定义用户信息,可以根据需要把用户数据权限,访问权限等一同封装进去
 *
 * @author mjr
 * @date 2024/12/13 16:49
 **/
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class SpringSecurityActiveUser extends User implements Serializable {

    private static final long serialVersionUID = 1L;

    private Integer userId;

    private String username;

    private String password;

    private String nickName;

    private Integer age;

    private String sex;

    private String address;

    private String avatar;

    private BigDecimal account;

    public SpringSecurityActiveUser(Integer userId, String username, String password, String nickName,
                                    Integer age, String sex, String address, String avatar, BigDecimal account,
                                    Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
        this.userId = userId;
        this.username = username;
        this.password = password;
        this.nickName = nickName;
        this.age = age;
        this.sex = sex;
        this.avatar = avatar;
        this.address = address;
        this.account = account;
    }
}
/**
 * redis服务组件
 *
 * @author: mjr
 * @date: 2024/12/10 15:10
 */
@Component
public class RedisComponent {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 把数据存入redis
     *
     * @param key
     * @param obj
     * @param timeout 超时时间 单位:分钟
     */
    public void setObj(final String key, Object obj, long timeout) {
        ValueOperations<String, Object> operations = redisTemplate.opsForValue();
        operations.set(key, obj, timeout, TimeUnit.SECONDS);
    }

    /**
     * 把数据存入redis
     *
     * @param key
     * @param obj
     * @param timeout  超时时间
     * @param timeUnit 单位
     */
    public void setObj(final String key, Object obj, long timeout, TimeUnit timeUnit) {
        ValueOperations<String, Object> operations = redisTemplate.opsForValue();
        operations.set(key, obj, timeout, timeUnit);
    }

    /**
     * 把数据存入redis
     *
     * @param key
     * @param obj
     */
    public void setObj(final String key, Object obj) {
        ValueOperations<String, Object> operations = redisTemplate.opsForValue();
        operations.set(key, obj);
    }

    /**
     * 从redis拿数据
     *
     * @param key
     * @return
     */
    public Object getObj(final String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 从redis中删除数据
     *
     * @param key 可以传一个值或多个值
     * @return
     */
    @SuppressWarnings("unchecked")
    public void removeObj(final String... key) {
        if (key.length == 1) {
            redisTemplate.delete(key[0]);
        } else {
            redisTemplate.delete(Arrays.asList(key));
        }
    }
}
/**
 * Redis数据库相关常量
 *
 * @author: mjr
 * @date: 2024/12/10 15:15
 */
public class RedisConstants {

    public static final String KNOWLEDGR_BASE_SECURITY = "KNOWLEDGR_BASE_SECURITY:SECURITY:";

    public static final String ACCESS_TOKEN_KEY = KNOWLEDGR_BASE_SECURITY + "access_token_key_";

    public static final String REFRESH_TOKEN_KEY = KNOWLEDGR_BASE_SECURITY + "refresh_token_key_";

    public static final String ACTIVE_USER_KEY = KNOWLEDGR_BASE_SECURITY + "active_user_key_";

    public static final String CAPTCHA_KEY = KNOWLEDGR_BASE_SECURITY + "captcha_key_";

}
/**
 * 加解密相关常量
 */
public final class SecretConstants {

    /**
     * 数据加密RSA公钥
     */
    public static final String DATA_RSA_PUBLIC_KEY = PropertiesUtils.readProperty("secret.properties",
            "public_rsa_key");

    /**
     * 数据解密RSA私钥
     */
    public static final String DATA_RSA_PRIVATE_KEY = PropertiesUtils.readProperty("secret.properties",
            "private_rsa_key");

    /**
     * 数据验签公钥
     */
    public static final String SIGN_RSA_PUBLIC_KEY = PropertiesUtils.readProperty("secret.properties",
            "public_rsa_sign_key");

    /**
     * 数据签名私钥
     */
    public static final String SIGN_RSA_PRIVATE_KEY = PropertiesUtils.readProperty("secret.properties",
            "private_rsa_sign_key");

}
/**
 * java读取.properties文件
 */
public class PropertiesUtils {

    private final static Logger LOGGER = LoggerFactory.getLogger(PropertiesUtils.class);

    /**
     * 读取资源属性文件(properties),无缓存方式
     *
     * @param filePath
     * @param param
     * @return
     */
    public static String readPropertyNoCache(String filePath, String param) {
        try {
            String url = PropertiesUtils.class.getResource("/").getPath() + filePath;
            Properties prop = new Properties();
            InputStream in = new BufferedInputStream(new FileInputStream(url));
            prop.load(new InputStreamReader(in, "utf-8"));
            return prop.getProperty(param);
        } catch (IOException e) {
            LOGGER.error("读properties属性文件异常!", e);
        }
        return null;
    }

    /**
     * 读取资源属性文件(properties),用IO流的方式
     *
     * @param filePath
     * @param param
     * @return
     */
    public static String readProperty(String filePath, String param) {
        // 属性集合对象
        Properties properties = new Properties();
        // 获取路径并转换成流
        InputStream path = Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath);
        try {
            // 将属性文件流装载到Properties对象中
            properties.load(path);
            return properties.getProperty(param);
        } catch (IOException e) {
            LOGGER.error("读properties属性文件异常!", e);
        }
        return null;
    }

    /**
     * 读取资源属性文件(properties),然后根据.properties文件的名称信息(本地化信息)
     *
     * @param filePath
     * @param param
     * @return
     */
    public static String getProperty(String filePath, String param) {
        ResourceBundle resourceBundle = ResourceBundle.getBundle(filePath);
        return resourceBundle.getString(param);
    }

    /**
     * 读取.properties配置文件的内容至Map中
     *
     * @param propertiesFile
     * @param param
     * @return
     */
    public static Map<String, String> read2Map(String propertiesFile, String param) {
        ResourceBundle rb = ResourceBundle.getBundle(propertiesFile);
        Map<String, String> map = new HashMap<String, String>(16);
        Enumeration<String> enu = rb.getKeys();
        while (enu.hasMoreElements()) {
            String obj = enu.nextElement();
            // 传了参数
            if (StringUtils.isNotEmpty(param)) {
                if (obj.indexOf(param) != -1) {
                    String objv = rb.getString(obj);
                    map.put(obj, objv);
                }
            }
            // 没传参数
            else {
                String objv = rb.getString(obj);
                map.put(obj, objv);
            }
        }
        return map;
    }

    /**
     * 写properties文件
     *
     * @param filePath
     * @param pKey
     * @param pValue
     * @return
     */
    public static boolean writeProperties(String filePath, String pKey, String pValue) {
        try {
            String url = PropertiesUtils.class.getResource("/").getPath() + filePath;
            Properties prop = new Properties();
            InputStream in = new BufferedInputStream(new FileInputStream(url));
            // 将属性文件流装载到Properties对象中
            prop.load(in);
            // 调用 Hashtable 的方法 put。使用 getProperty 方法提供并行性。
            // 强制要求为属性的键和值使用字符串。返回值是 Hashtable 调用 put 的结果。
            OutputStream out = new FileOutputStream(url);
            prop.setProperty(pKey, pValue);
            // 以适合使用 load 方法加载到 Properties 表中的格式,
            // 将此 Properties 表中的属性列表(键和元素对)写入输出流
            prop.store(out, "Update " + pKey + " name");
            return true;
        } catch (Exception e) {
            LOGGER.error("写properties属性文件异常!", e);
            return false;
        }
    }
}

secret.properties文件内容

image.png

secret.properties文件里的值可以去在线网站生成密钥对 web.chacuo.net/netrsakeypa… 也可以代码生成

代码生成

import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;

import org.apache.commons.codec.binary.Base64;

/**
 * 公钥私钥生成程序
 * 
 * @author: mjr
 * @date: 2024/12/6 10:32
 */
public class RSAUtil {
    private static final int KEY_SIZE = 1024;

    public static void main(String[] args) throws Exception {
        // 生成密钥对
        KeyPair keyPair = generateKeyPair();
        PublicKey publicKey = keyPair.getPublic();
        String publicKeyStr = Base64.encodeBase64URLSafeString(publicKey.getEncoded());
        PrivateKey privateKey = keyPair.getPrivate();
        String privateKeyStr = Base64.encodeBase64URLSafeString(privateKey.getEncoded());
        System.out.println(publicKeyStr);
        System.out.println("-------------------");
        System.out.println(privateKeyStr);

        System.out.println("-------------------");
        // 原始数据
        String originalData = "This is a secret message.";
        byte[] data = originalData.getBytes();

        // 加密
        String encryptedData = encrypt(data, publicKey);
        System.out.println("Encrypted Data: " + encryptedData);

        // 解密
        String decryptedData = decrypt(encryptedData, privateKey);
        System.out.println("Decrypted Data: " + decryptedData);
    }

    /**
     * 生成密钥对
     *
     * @return
     * @throws Exception
     */
    private static KeyPair generateKeyPair() throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(KEY_SIZE);
        return keyPairGenerator.generateKeyPair();
    }

    /**
     * 公钥加密数据
     *
     * @param data
     * @param publicKey
     * @return
     * @throws Exception
     */
    private static String encrypt(byte[] data, PublicKey publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        return Base64.encodeBase64URLSafeString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data, KEY_SIZE));
    }

    /**
     * 私钥解密数据
     *
     * @param encryptedData
     * @param privateKey
     * @return
     * @throws Exception
     */
    private static String decrypt(String encryptedData, PrivateKey privateKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(encryptedData), KEY_SIZE), "UTF-8");
    }

    /**
     * 将数据分块
     *
     * @param cipher
     * @param opmode
     * @param datas
     * @param keySize
     * @return
     */
    private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] datas, int keySize) {
        int maxBlock = 0;
        if (opmode == Cipher.DECRYPT_MODE) {
            maxBlock = keySize / 8;
        } else {
            maxBlock = keySize / 8 - 11;
        }

        int offSet = 0;
        byte[] buff;
        byte[] resultDatas;
        int i = 0;
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            while (datas.length > offSet) {
                if (datas.length - offSet > maxBlock) {
                    buff = cipher.doFinal(datas, offSet, maxBlock);
                } else {
                    buff = cipher.doFinal(datas, offSet, datas.length - offSet);
                }
                out.write(buff, 0, buff.length);
                i++;
                offSet = i * maxBlock;
            }
            resultDatas = out.toByteArray();
        } catch (Exception e) {
            throw new RuntimeException("加解密阀值为[" + maxBlock + "]的数据时发生异常", e);
        }
        return resultDatas;
    }
}
/**
 * JWT工具类
 *
 * @author mjr
 * @date 2024/12/11 15:43
 **/
@Slf4j
@Component
public class JwtTokenUtils {

    @Resource
    private JwtProperties jwtProperties;

    /**
     * 生成 accessToken(初始化Token令牌)
     *
     * @param uuid
     * @param account 用户账号
     * @return
     */
    public String createAccessToken(String uuid, String account) throws Exception {
        // 当前时间
        Date currentDate = new Date();
        // Base64 编码的私钥字符串
        String privateKeyStr = SecretConstants.SIGN_RSA_PRIVATE_KEY;

        // 将 Base64 编码的私钥字符串转换为 PrivateKey 对象
        byte[] privateKeyBytes = Base64.decode(privateKeyStr);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
        // 登陆成功生成JWT
        return Jwts.builder()
                // 放入appId
                .setId(uuid)
                // 主题
                .setSubject(account)
                // 签发时间
                .setIssuedAt(currentDate)
                // 签发者
                .setIssuer("mjr")
                // 自定义属性 放入用户拥有权限
                // .claim("authorities", JSON.toJSONString(activeUser.getAuthorities()))
                // 失效时间
                .setExpiration(DateUtil.offsetSecond(currentDate, this.jwtProperties.getAccessTokenExpiration()))
                // 签名算法和密钥,使用 RS256 算法和私钥签名
                .signWith(SignatureAlgorithm.RS256, privateKey)
                .compact();
    }

    /**
     * 生成 refreshToken
     *
     * @param accessToken 原令牌
     * @return
     */
    public String creatRefreshToken(String accessToken) {
        // 当前时间
        Date currentDate = new Date();
        String refreshedToken;
        try {
            Claims claims = this.getClaimsFromToken(accessToken);
            if (claims == null) {
                return null;
            }
            claims.put(Claims.ISSUED_AT, currentDate);
            // Base64 编码的私钥字符串
            String privateKeyStr = SecretConstants.SIGN_RSA_PRIVATE_KEY;
            // 将 Base64 编码的私钥字符串转换为 PrivateKey 对象
            byte[] privateKeyBytes = Base64.decode(privateKeyStr);
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
            refreshedToken = Jwts.builder()
                    .setClaims(claims)
//                    .claim("custom", Base64.decodeStr(claims.getId() + claims.getSubject(), StandardCharsets.UTF_8))
                    // 签发时间
                    .setIssuedAt(currentDate)
                    // 失效时间
                    .setExpiration(DateUtil.offsetSecond(currentDate, this.jwtProperties.getRefreshTokenExpiration()))
                    .signWith(SignatureAlgorithm.RS256, privateKey)
                    .compact();
        } catch (Exception e) {
            log.error(e.getMessage());
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 从token中解析出数据,验证 JWT(使用公钥)
     *
     * @param token 令牌
     * @return
     */
    public Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            // 将 Base64 编码的公钥字符串转换为 PublicKey 对象
            byte[] publicKeyBytes = Base64.decode(SecretConstants.SIGN_RSA_PUBLIC_KEY);
            PublicKey publicKey = KeyUtil.generatePublicKey("RSA", publicKeyBytes);
            claims = Jwts.parser()
                    // 公钥验签
                    .setSigningKey(publicKey)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            log.error(e.getMessage());
            claims = null;
        }
        return claims;
    }

    /**
     * 从令牌中获取 appId
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getAppIdFromToken(String token) {
        String appId;
        try {
            Claims claims = this.getClaimsFromToken(token);
            if (claims == null) {
                return null;
            }
            appId = claims.getId();
        } catch (Exception e) {
            log.error(e.getMessage());
            appId = null;
        }
        return appId;
    }

    /**
     * 从令牌中获取用户账号
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getAccountFromToken(String token) {
        String username;
        try {
            Claims claims = this.getClaimsFromToken(token);
            if (claims == null) {
                return null;
            }
            username = claims.getSubject();
        } catch (Exception e) {
            log.error(e.getMessage());
            username = null;
        }
        return username;
    }

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return 是否过期
     */
    public Boolean isTokenExpired(String token) {
        try {
            Claims claims = this.getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            return true;
        }
    }

    /**
     * 验证令牌是否有效
     *
     * @param accountParam 账号
     * @param token        令牌
     * @return
     */
    public Boolean validateToken(String accountParam, String token) {
        String account = this.getAccountFromToken(token);
        return StringUtils.equals(account, accountParam) && !this.isTokenExpired(token);
    }
}

application.yml文件也可以直接写到代码里去,这样更灵活

# JWT配置
jwt:
  # accessToken过期时间,单位秒 1天后过期=86400 7天后过期=604800
  accessTokenExpiration: 60
  # refreshToken过期时间,单位秒 1天后过期=86400 7天后过期=604800
  refreshTokenExpiration: 604800
  # 不需要认证的接口
  antMatchers:
    - /login
    - /user/login
    - /login/getCaptcha
    - /register