Spring Security+JWT 实现登录授权

一、实现JWT

  1. 封装jwt信息
package com.travel.security.authorize;

import lombok.Data;

/**
 * @author lirenqi
 * @date 2024/11/9
 */
@Data
public class JWTTokenInfo {

    // token
    private String token;
    // token类型
    private String tokenType;
    // 过期时间戳(秒)
    private long expiresIn;
    // 状态(生效,过期)
    private int status;
}

  1. JWT工具类
package com.travel.security.authorize;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.util.Date;

/**
 * 生成和验证 JWT token
 *
 * @author lirenqi
 * @date 2024/11/9
 */
@Slf4j
@Component
public class JwtTokenService {

    // 令牌自定义标识
    @Value("${jwt.header}")
    private String header;

    @Value("${jwt.secret}")
    private String secretKey;

    // 以分钟为单位
    @Value("${jwt.expiration}")
    private long expiration;

    public JWTTokenInfo getToken(String username) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            return generationToken(username);
        }
        // 如果认证信息有效,返回现有的 token(无论是有效还是即将过期的)
        // 从认证信息中提取已有 token
        String existingToken = authentication.getCredentials().toString();
        // 获取现有 token 的过期时间
        long expiresIn = getTokenExpirationTime(existingToken);

        // 如果现有 token 已经过期,生成新的 token
        if (expiresIn <= 0) {
            return generationToken(username);
        }
        // 如果现有 token 仍然有效,返回现有 token 信息
        JWTTokenInfo jwtTokenInfo = new JWTTokenInfo();
        jwtTokenInfo.setToken(existingToken);
        jwtTokenInfo.setExpiresIn(expiresIn);
        return jwtTokenInfo;
    }

    public JWTTokenInfo generationToken(String username) {
        Claims claims = Jwts.claims().setSubject(username);

        // 获取过期时间
        Date now = new Date();
        long expiresIn = (now.getTime() / 1000) + (expiration * 60);
        Date validity = new Date(expiresIn * 1000);

        // 生成token
        String token = Jwts.builder().setClaims(claims).setIssuedAt(now).setExpiration(validity).signWith(SignatureAlgorithm.HS256, secretKey.getBytes(StandardCharsets.UTF_8)).compact();

        // 获取过期时间
        Claims generationAfter = Jwts.parser().setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();
        Date expirationDate = generationAfter.getExpiration();
        long expires = (expirationDate.getTime() - System.currentTimeMillis()) / 1000;

        // 返回token信息
        JWTTokenInfo tokenInfo = new JWTTokenInfo();
        tokenInfo.setTokenType("Bearer");
        tokenInfo.setToken(token);
        tokenInfo.setExpiresIn(expires);

        return tokenInfo;
    }

    public boolean verifyToken(String token) {
        try {
            Jwts.parser().setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            log.warn("令牌验证失败: {}", e.getMessage());
            return false;
        }
    }

    /**
     * 获取登录对象信息
     *
     * @param token token
     * @return 登录信息
     */
    public String getClientIdFromToken(String token) {
        try {
            Claims claims = Jwts.parser().setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();
            return claims.getSubject();
        } catch (Exception e) {
            System.out.println("解析token失败: " + e.getMessage());
            return null;
        }
    }

    /**
     * 获取token的有效期(以秒为单位)
     *
     * @param token token
     * @return 有效期(以秒为单位)
     */
    public long getTokenExpirationTime(String token) {
        try {
            Claims claims = Jwts.parser().setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();
            Date expirationDate = claims.getExpiration();
            if (expirationDate != null) {
                long now = System.currentTimeMillis();
                long expiresInSeconds = (expirationDate.getTime() - now) / 1000;
                return expiresInSeconds;
            }
        } catch (Exception e) {
            log.warn("解析token失败: " + e.getMessage());
        }
        return 0;
    }
}
  1. 创建一个提供者来生成和验证JWT令牌。
package com.travel.security.authorize;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.util.Date;

/**
 * 生成和验证 JWT token
 *
 * @author lirenqi
 * @date 2024/11/9
 */
@Slf4j
@Component
public class JwtTokenService {

    // 令牌自定义标识
    @Value("${jwt.header}")
    private String header;

    @Value("${jwt.secret}")
    private String secretKey;

    // 以分钟为单位
    @Value("${jwt.expiration}")
    private long expiration;

    public JWTTokenInfo getToken(String username) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            return generationToken(username);
        }
        // 如果认证信息有效,返回现有的 token(无论是有效还是即将过期的)
        // 从认证信息中提取已有 token
        String existingToken = authentication.getCredentials().toString();
        // 获取现有 token 的过期时间
        long expiresIn = getTokenExpirationTime(existingToken);

        // 如果现有 token 已经过期,生成新的 token
        if (expiresIn <= 0) {
            return generationToken(username);
        }
        // 如果现有 token 仍然有效,返回现有 token 信息
        JWTTokenInfo jwtTokenInfo = new JWTTokenInfo();
        jwtTokenInfo.setToken(existingToken);
        jwtTokenInfo.setExpiresIn(expiresIn);
        return jwtTokenInfo;
    }

    public JWTTokenInfo generationToken(String username) {
        Claims claims = Jwts.claims().setSubject(username);

        // 获取过期时间
        Date now = new Date();
        long expiresIn = (now.getTime() / 1000) + (expiration * 60);
        Date validity = new Date(expiresIn * 1000);

        // 生成token
        String token = Jwts.builder().setClaims(claims).setIssuedAt(now).setExpiration(validity).signWith(SignatureAlgorithm.HS256, secretKey.getBytes(StandardCharsets.UTF_8)).compact();

        // 获取过期时间
        Claims generationAfter = Jwts.parser().setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();
        Date expirationDate = generationAfter.getExpiration();
        long expires = (expirationDate.getTime() - System.currentTimeMillis()) / 1000;

        // 返回token信息
        JWTTokenInfo tokenInfo = new JWTTokenInfo();
        tokenInfo.setTokenType("Bearer");
        tokenInfo.setToken(token);
        tokenInfo.setExpiresIn(expires);

        return tokenInfo;
    }

    public boolean verifyToken(String token) {
        try {
            Jwts.parser().setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            log.warn("令牌验证失败: {}", e.getMessage());
            return false;
        }
    }

    /**
     * 获取登录对象信息
     *
     * @param token token
     * @return 登录信息
     */
    public String getClientIdFromToken(String token) {
        try {
            Claims claims = Jwts.parser().setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();
            return claims.getSubject();
        } catch (Exception e) {
            System.out.println("解析token失败: " + e.getMessage());
            return null;
        }
    }

    /**
     * 获取token的有效期(以秒为单位)
     *
     * @param token token
     * @return 有效期(以秒为单位)
     */
    public long getTokenExpirationTime(String token) {
        try {
            Claims claims = Jwts.parser().setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();
            Date expirationDate = claims.getExpiration();
            if (expirationDate != null) {
                long now = System.currentTimeMillis();
                long expiresInSeconds = (expirationDate.getTime() - now) / 1000;
                return expiresInSeconds;
            }
        } catch (Exception e) {
            log.warn("解析token失败: " + e.getMessage());
        }
        return 0;
    }
}
  1. 创建一个过滤器来拦截请求并检查是否存在有效的JWT令牌。
package com.travel.security.authorize;

import com.travel.common.utils.SecurityUtils;
import com.travel.security.AuthInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
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;

/**
 * token过滤器 验证token有效性
 *
 * @author huoyaoo
 */
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenService jwtTokenService;
    // 令牌自定义标识
    @Value("${jwt.header}")
    private String header;

    @Autowired
    private AuthInfo authInfo;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        // 放心获取token的接口
        String token = request.getHeader(header);
        if (token.startsWith("Basic ")) {
            chain.doFilter(request, response);
            return;
        }

        // 删除Bearer
        String[] split = token.split(" ");
        token = split[1];
        // 清除之前的认证上下文,避免脏数据
        SecurityContextHolder.clearContext();
        if (token == null) {
            chain.doFilter(request, response);
            return;
        }

        // 校验token合法性
        String clientId = jwtTokenService.getClientIdFromToken(token);
        Authentication authentication = SecurityUtils.getAuthentication();

        if (clientId == null) {
            chain.doFilter(request, response);
            return;
        }
        // 验证token是否有效
        boolean existence = jwtTokenService.verifyToken(token);
        if (!existence) {
            chain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(clientId, null, authInfo.getAuthorities());
        // token验证成功后保留到上下文
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        chain.doFilter(request, response);
    }
}

二、Spring Security 配置

  1. 统一处理当用户尝试访问受保护的资源但没有提供正确的认证信息时,Spring Security 会调用这个类来处理如何响应用户的请求。
package com.travel.security;

import com.alibaba.fastjson.JSON;
import com.travel.common.constant.HttpStatus;
import com.travel.common.core.domain.AjaxResult;
import com.travel.common.utils.ServletUtils;
import com.travel.common.utils.StringUtils;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

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

/**
 * 认证失败处理类 返回未授权
 * 
 * @author huoyaoo
 */
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable
{
    private static final long serialVersionUID = -8970718410437077606L;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
            throws IOException
    {
        int code = HttpStatus.UNAUTHORIZED;
        String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
    }
}
  1. 用户信息类;这里是从配置文件加载用户名和密码的,具体根据自己的场景来加载用户名和密码
package com.travel.security;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collection;

/**
 * 飞思应用信息
 *
 * @author lirenqi
 * @date 2024/11/9
 */
@Component
@ConfigurationProperties(prefix = "face")
@Data
public class AuthInfo implements UserDetails {
    // 应用id
    private String clientId;
    // 应用密钥
    private String clientSecret;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 返回该客户端的权限(如果有的话)
        return new ArrayList<>();
    }

    @Override
    public String getPassword() {
        // 客户端密钥可以看作是一个密码
        return clientSecret;
    }

    @Override
    public String getUsername() {
        // 客户端ID就是用户名
        return clientId;
    }

    @Override
    public boolean isAccountNonExpired() {
        // 默认不过期
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        // 默认不锁定
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        // 默认不过期
        return true;
    }

    @Override
    public boolean isEnabled() {
        // 默认启用
        return true;
    }

}

  1. 校验用户的登录信息是否合法
package com.travel.security;

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 javax.annotation.Resource;

/**
 * @author lirenqi
 * @date 2024/11/9
 */
@Service
public class MyUserDetailsService implements UserDetailsService {

    @Resource
    private AuthInfo authInfo;

    @Override
    public UserDetails loadUserByUsername(String clientId) throws UsernameNotFoundException {
        if (clientId.equals(authInfo.getClientId())) {
            return authInfo;
        } else {
            throw new UsernameNotFoundException("非法的应用!");
        }
    }
}

  1. 配置Spring Security安全配置类
package com.travel.security;

import com.travel.security.authorize.JwtAuthenticationTokenFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;

/**
 * 安全配置类
 *
 * @author lirenqi
 * @date 2024/11/9
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * token认证过滤器
     */
    @Resource
    private JwtAuthenticationTokenFilter authenticationTokenFilter;

    @Resource
    private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;

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

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 不需要权限能访问的资源
        web.ignoring()
                // 接口放行
                .antMatchers("/login")
                .antMatchers("/check/token/expiration");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                // 允许 /login 接口 匿名访问,anonymous里面访问
                .antMatchers("/login", "/check/token/expiration").anonymous()
                // 其他所有请求需要认证
                .anyRequest().authenticated().and()
                // 启用 HTTP Basic 认证
                .httpBasic()
                .and()
                // 禁用 CSRF 保护(仅在开发环境中使用)
                .csrf().disable()
                // 定制如何处理认证和授权相关的异常(例如,认证失败、访问权限不足等)
                .exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint)
                .and()
                // 添加自定义过滤器
                .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

三、编写controller

  1. 登录,或者token接口
package com.travel.controller;

import com.travel.common.view.AjaxResultL;
import com.travel.domain.dto.EventAuthDto;
import com.travel.domain.resp.TokenRes;
import com.travel.security.authorize.JWTTokenInfo;
import com.travel.security.authorize.JwtTokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Base64;

/**
 *  登录授权
 *
 * @author lirenqi
 * @date 2024/11/9
 */
@Slf4j
@RestController
public class AuthorizationController {

    @Resource
    private JwtTokenService jwtTokenService;
    @Resource
    private UserDetailsService userDetailsService;

    /**
     * 登录
     * @param dto 登录信息
     * @return token
     */
    @PostMapping("/login")
    public TokenRes faceEventHandel(HttpServletRequest request, EventAuthDto dto) {
        try {
            log.info("获取token...");
            // 解析请求头
            String authorizationHeader = request.getHeader("Authorization");
            String base64Credentials = authorizationHeader.substring(6);
            String credentials = new String(Base64.getDecoder().decode(base64Credentials));

            // 解析用户名和密码
            final String[] values = credentials.split(":", 2);
            String username = values[0];
            String password = values[1];

            // 校验登录信息
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (password.equals(userDetails.getPassword())) {
                // 生成令牌
                JWTTokenInfo tokenInfo = jwtTokenService.getToken(username);

                TokenRes tokenRes = new TokenRes();
                tokenRes.setAccessToken(tokenInfo.getToken());
                tokenRes.setTokenType("Bearer");
                tokenRes.setExpiresIn(tokenInfo.getExpiresIn());
                tokenRes.setScope(dto.getScope());
                return tokenRes;
            } else {
                log.warn("密钥不匹配:{},{}", username, password);
            }
        } catch (UsernameNotFoundException | BadCredentialsException e) {
            log.warn("认证失败{0}", e);
            // 处理认证失败的情况
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, e.getMessage(), e);
        }
        return null;
    }

    /**
     * 获取token过期时间
     * @param token token
     * @return 过期时间
     */
    @PostMapping("/check/token/expiration")
    public AjaxResultL<Long> checkTokenExpiration(String token) {
        try {
            long expiresIn = jwtTokenService.getTokenExpirationTime(token);
            return AjaxResultL.success(expiresIn);
        } catch (Exception e) {
            log.error("接口处理失败:", e);
        }
        return AjaxResultL.error();
    }

    /**
     * 测试授权可用否
     * @return 过期时间
     */
    @PostMapping("/auth/test")
    public AjaxResultL<Void> checkTokenExpiration() {
        try {
            return AjaxResultL.success();
        } catch (Exception e) {
            log.error("接口处理失败:", e);
        }
        return AjaxResultL.error();
    }
}

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容