spring security+JWT实战

现在大多都是前后端分离的项目了,系统权限认证是最开始就需要考虑的问题之一,下面是前段时间搓小项目时的spring security+JWT实战应用。

第一步,先将要用到的依赖导入,采用的是spring3.5.7和jjwt0.12.5。

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-security</artifactId>

</dependency>

<dependency>

    <groupId>io.jsonwebtoken</groupId>

    <artifactId>jjwt-api</artifactId>

</dependency>

<dependency>

    <groupId>io.jsonwebtoken</groupId>

    <artifactId>jjwt-impl</artifactId>

    <scope>runtime</scope>

</dependency>

<dependency>

    <groupId>io.jsonwebtoken</groupId>

    <artifactId>jjwt-jackson</artifactId>

    <scope>runtime</scope>

</dependency>

第二步,新增一个SecurityConfig配置类对Spring Security做定制配置,主要是鉴权策略、session策略、密码加密算法等。

import cn.ghw.web.filter.JwtAuthenticationFilter;

import lombok.RequiredArgsConstructor;

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.configuration.AuthenticationConfiguration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;

import org.springframework.security.config.http.SessionCreationPolicy;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.security.web.SecurityFilterChain;

import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration

@EnableWebSecurity

@RequiredArgsConstructor

public class SecurityConfig {

    @Bean

    public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception {

        http.csrf(AbstractHttpConfigurer::disable)

                .authorizeHttpRequests(auth -> auth

                        .requestMatchers("/auth/**").permitAll()

                        .anyRequest().authenticated()

                )

                .sessionManagement(session -> session

                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                );

        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();

    }

    @Bean

    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {

        return authenticationConfiguration.getAuthenticationManager();

    }

    @Bean

    public PasswordEncoder passwordEncoder() {

        return new BCryptPasswordEncoder();

    }

}

第三步,新增一个JwtTokenService封装token相关功能支持。

import cn.ghw.web.common.SysConstants;

import cn.ghw.web.common.enums.AuthTypeEnum;

import io.jsonwebtoken.Claims;

import io.jsonwebtoken.Jwts;

import io.jsonwebtoken.security.Keys;

import io.jsonwebtoken.security.SecureDigestAlgorithm;

import lombok.extern.slf4j.Slf4j;

import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;

import java.security.SecureRandom;

import java.util.Date;

import java.util.Objects;

import java.util.function.Function;

@Slf4j

@Service

public class JwtTokenService {

    private static final long ACCESS_TOKEN_EXPIRE = 30 * 60 * 1000L; // 30分钟

    private static final long REFRESH_TOKEN_EXPIRE = 7 * 24 * 60 * 60 * 1000L; // 7天

    private static final SecureDigestAlgorithm<SecretKey, SecretKey> ALGORITHM = Jwts.SIG.HS256;

    private static final byte[] keyBytes = new byte[32];

    static {

        new SecureRandom().nextBytes(keyBytes);

    }

    private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(keyBytes);

    /**

     * 生成AccessToken

     */

    public String generateAccessToken(Long userId, AuthTypeEnum authType) {

        return Jwts.builder()

                .subject(String.valueOf(userId))

                .claim("userId", userId)

                .claim("authType", authType.getCode())

                .issuedAt(new Date(System.currentTimeMillis()))

                .expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRE))

                .signWith(SECRET_KEY, ALGORITHM)

                .compact();

    }

    /**

     * 生成RefreshToken

     */

    public String generateRefreshToken(Long userId) {

        return Jwts.builder()

                .subject(String.valueOf(userId))

                .claim(SysConstants.USERID_KEY, userId)

                .issuedAt(new Date(System.currentTimeMillis()))

                .expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRE))

                .signWith(SECRET_KEY, ALGORITHM)

                .compact();

    }

    public boolean validateToken(String token, Long userId) {

        Claims claims = getAllClaimsFromToken(token);

        return Objects.equals(claims.get("userId", Long.class), userId);

    }

    public Date getExpirationDateFromToken(String token) {

        return getClaimFromToken(token, Claims::getExpiration);

    }

    public boolean isTokenExpired(String token) {

        try {

            final Date expiration = getExpirationDateFromToken(token);

            return expiration.before(new Date());

        } catch (Exception e) {

            log.error("检查token是否失效异常:{}", e.getMessage());

            return true;

        }

    }

    public Claims getAllClaimsFromToken(String token) {

        return Jwts.parser()

                .verifyWith(SECRET_KEY)

                .build()

                .parseSignedClaims(token).getPayload();

    }

    // 从Token中获取指定信息

    private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {

        final Claims claims = getAllClaimsFromToken(token);

        return claimsResolver.apply(claims);

    }

}

第四步,新增一个拦截校验token的拦截器类JwtAuthenticationFilter。

import cn.ghw.web.service.JwtTokenService;

import io.jsonwebtoken.Claims;

import jakarta.servlet.FilterChain;

import jakarta.servlet.ServletException;

import jakarta.servlet.http.HttpServletRequest;

import jakarta.servlet.http.HttpServletResponse;

import lombok.NonNull;

import lombok.RequiredArgsConstructor;

import lombok.extern.slf4j.Slf4j;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

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 java.io.IOException;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

@Slf4j

@RequiredArgsConstructor

@Component

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenService jwtTokenService;

    @Override

    protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {

        final String header = request.getHeader("Authorization");

        // JWT Token格式: "Bearer token"

        if (header != null && header.startsWith("Bearer")) {

            try {

                Long userId;

                String token = header.replace("Bearer ", "");

                // 检查token是否失效

                if (!jwtTokenService.isTokenExpired(token)) {

                    Claims claims = jwtTokenService.getAllClaimsFromToken(token);

                    userId = claims.get("userId", Long.class);

                    if (userId != null && jwtTokenService.validateToken(token, userId)) {

                        Map<String, Object> principal = new HashMap<>();

                        principal.put("userId", userId);

                        UsernamePasswordAuthenticationToken authentication =

                                new UsernamePasswordAuthenticationToken(

                                        principal,

                                        null,

                                        List.of());

                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                        SecurityContextHolder.getContext().setAuthentication(authentication);

                    }

                } else {

                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

                    return;

                }

            } catch (Exception e) {

                log.error("Unable to get JWT Token:{}", e.getMessage());

                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

                return;

            }

        }

        filterChain.doFilter(request, response);

    }

}

第五步,实现登录业务和token刷新业务功能即可。

import cn.ghw.web.common.R;

import cn.ghw.web.common.enums.AuthTypeEnum;

import cn.ghw.web.common.exception.BusinessException;

import cn.ghw.web.dto.LoginDto;

import cn.ghw.web.dto.RegisterReq;

import cn.ghw.web.entity.UserEntity;

import cn.ghw.web.service.UserService;

import cn.ghw.web.vo.LoginResp;

import jakarta.validation.Valid;

import jakarta.validation.constraints.NotEmpty;

import lombok.RequiredArgsConstructor;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;

import org.springframework.http.HttpHeaders;

import org.springframework.http.HttpStatus;

import org.springframework.http.ResponseEntity;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.validation.annotation.Validated;

import org.springframework.web.bind.annotation.*;

import java.util.Collections;

@Slf4j

@Validated

@RequiredArgsConstructor

@RestController

@RequestMapping("/auth")

public class WebAuthController {

    private final UserService userService;

    private final PasswordEncoder passwordEncoder;

    @PostMapping("/login")

    public ResponseEntity<R> login(@RequestBody @Valid LoginDto req) {

        try {

            // 1.验证账号

            String account = req.getAccount();

            UserEntity user = userService.queryUserByAccount(account);

            if (user == null) {

                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(R.fail("用户不存在"));

            }

            // 2.验证密码

            String password = req.getPassword();

            if (!passwordEncoder.matches(password, user.getPasswordHash())) {

                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(R.fail("密码错误"));

            }

            // 3. 认证

            LoginResp loginResp = userService.authentication(AuthTypeEnum.PASSWORD, user.getUserId(), account);

            if (loginResp == null || StringUtils.isEmpty(loginResp.getAccessToken()) || StringUtils.isEmpty(loginResp.getRefreshToken())) {

                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(R.fail("登录失败"));

            }

            HttpHeaders headers = new HttpHeaders();

            headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + loginResp.getAccessToken());

            headers.setAccessControlExposeHeaders(Collections.singletonList(HttpHeaders.AUTHORIZATION));

            return ResponseEntity.ok()

                    .headers(headers).

                    body(R.success(loginResp));

        } catch (AuthenticationException e) {

            log.error("生成token异常:{}", e.getMessage());

            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(R.fail("获取登录失败"));

        }

    }

    @GetMapping("refresh")

    public ResponseEntity<String> refreshToken(@RequestParam @NotEmpty(message = "refreshToken不能为空") String refreshToken) {

        try {

            String accessToken = userService.refreshToken(AuthTypeEnum.PASSWORD, refreshToken);

            if (StringUtils.isEmpty(accessToken)) {

                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh token expired");

            }

            return ResponseEntity.ok(accessToken);

        } catch (Exception e) {

            log.error("刷新token异常:{}", e.getMessage());

            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh token error");

        }

    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值