当前位置: 首页 > article >正文

springboot3 spring security+jwt实现接口权限验证实现

背景

前一个项目基于springboot2做的后台服务,使用到了spring security做权限验证,token是用java生成的uuid,把token信息存储到了redis服务中。

新的项目计划使用springboot3,且希望使用JWT实现token,以下重新记录下实现思路。

项目依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.3.9</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.jy.bike</groupId>
	<artifactId>bike</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>bike</name>
	<description>Demo project for Spring Boot</description>
	<url/>


	<properties>
		<mysql-connector>8.0.18</mysql-connector>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

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

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

		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
			<version>3.5.10</version>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>${mysql-connector}</version>
		</dependency>

		<dependency>
			<groupId>org.springdoc</groupId>
			<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
			<version>2.2.0</version>
		</dependency>

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

		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-api</artifactId>
			<version>0.11.5</version>
		</dependency>

		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-impl</artifactId>
			<version>0.11.5</version>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-jackson</artifactId>
			<version>0.11.5</version>
		</dependency>


	</dependencies>


	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

Spring Security的鉴权原理

通过jwt生成token后,后续接口请求时,在header中传入jwt token,通过自定义JwtAuthenticationFilter获取登录用户信息,并放在spring security context里。由后续UsernamePasswordAuthenticationFilter验证和拦截鉴权

实现步骤

1.配置SecurityConfiguration

其中包括:白名单放行(swagger,login等资源),自定义JwtAuthenticationFilter并放在UsernamePasswordAuthenticationFilter之前,自定义UserService并通过其userDetailsService方法获取用户信息,使用BCryptPasswordEncoder密文验证账号密码,

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    @Autowired
    private UserService userService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // 关闭跨站请求
        http.csrf(AbstractHttpConfigurer::disable).authorizeHttpRequests(request ->
                // 配置放行白名单
                request.requestMatchers("login", "logout").permitAll()
                        .requestMatchers("swagger-ui/*", "v3/api-docs", "v3/api-docs/*", "/druid/**").permitAll().anyRequest().authenticated())
                // 禁用session
                .sessionManagement(manager -> manager.sessionCreationPolicy(STATELESS))
                .authenticationProvider(authenticationProvider()).addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

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

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userService.userDetailsService());
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

2.定义一个实体类,继承UserDetails类

用于放在spring security context里,里面包括登录账号的名称,密码,权限,状态等

public class LoginUserDetails implements UserDetails {
    private static final long serialVersionUID = 1L;

    private User user;

    public LoginUserDetails(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of();
    }

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

    @Override
    public String getUsername() {
        return user.getName();
    }

    @Override
    public boolean isEnabled() {
        return user.isEnabled();
    }
}

3.实现一个UserService,通过其loadUserByUsername获取用户

实际应该用username查询数据库获取,这里写死一个用户,并传入经过BCryptPasswordEncoder加密后的密文(原文是123456)

@Service
public class UserService {

    public UserDetailsService userDetailsService() {
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) {
                User curUser = new User();
                curUser.setName("madixin");                curUser.setPassword("$2a$10$Yt3wAk1P1aZZsJKnjGbnQehJD8F80tLS.tsenpPTC1kMrMdbjvN7.");
                curUser.setEnabled(true);
                LoginUserDetails loginUser = new LoginUserDetails(curUser);
                return loginUser;
            }
        };
    }
}

4.实现一个jwtservice,用于把用户信息加密和解密成token

@Service
public class JwtService implements IJwtService {
    @Value("${token.signing.key}")
    private String jwtSigningKey;
    @Override
    public String extractUserName(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    @Override
    public String generateToken(UserDetails userDetails) {
        return generateToken(new HashMap<>(), userDetails);
    }

    @Override
    public boolean isTokenValid(String token, UserDetails userDetails) {
        final String userName = extractUserName(token);
        return (userName.equals(userDetails.getUsername())) && !isTokenExpired(token);
    }

    private <T> T extractClaim(String token, Function<Claims, T> claimsResolvers) {
        final Claims claims = extractAllClaims(token);
        return claimsResolvers.apply(claims);
    }

    private String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
        return Jwts.builder().setClaims(extraClaims).setSubject(userDetails.getUsername())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 24))
                .signWith(getSigningKey(), SignatureAlgorithm.HS256).compact();
    }

    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token)
                .getBody();
    }

    private Key getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(jwtSigningKey);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

5.实现自己的login接口,验证登录账号和密码是否正确,是否禁用,如果通过,则使用jwtservice生成token返回。

调用authenticationManager.authenticate时,会自动调用UserService的loadUserByUsername获取用户和校验密码

@RestController
public class LoginController {
    private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class);

    @Autowired
    private LoginService loginService;

    /**
     * 登录方法
     *
     * @param loginDto 登录信息
     * @return 结果
     */
    @PostMapping("/login")
    public ResponseResult<String> login(@RequestBody LoginDto loginDto) {
        try {
            // 返回JWT令牌
            return ResponseResult.success(loginService.login(loginDto.getPhone(), loginDto.getPassword()));
        } catch (BikeBaseException e) {
            LOGGER.error(e.getMessage());
            return ResponseResult.fail(e.getErrorCode());
        }
    }
}


@Service
public class LoginService {
    private static final Logger LOGGER = LoggerFactory.getLogger(LoginService.class);

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtService jwtService;

    public String login(String username, String password) throws BikeBaseException {
        // 该方法会去调用UserDetailsService.loadUserByUsername
        Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        LoginUserDetails loginUser = (LoginUserDetails) authentication.getPrincipal();
        if (loginUser == null){
            throw new BikeBaseException(ErrorCode.ILLEGAL_AUTHENTICATE);// 认证失败
        }
        return jwtService.generateToken(loginUser);
    }

    public void logout() throws BikeBaseException {

    }
}

6.自实现JwtAuthenticationFilter(第一步已配置在UsernamePasswordAuthenticationFilter前),从header中获取token,如果验证通过,则把用户信息放在spring security context里。

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private JwtService jwtService;
    @Autowired
    private UserService userService;
    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request,
                                    @NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
            throws ServletException, IOException {
        final String authHeader = request.getHeader("Authorization");
        final String jwt;
        final String userEmail;
        if (StringUtils.isEmpty(authHeader) || !StringUtils.startsWith(authHeader, "Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
        jwt = authHeader.substring(7);
        userEmail = jwtService.extractUserName(jwt);
        if (StringUtils.isNotEmpty(userEmail)
                && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userService.userDetailsService()
                    .loadUserByUsername(userEmail);
            if (jwtService.isTokenValid(jwt, userDetails)) {
                SecurityContext context = SecurityContextHolder.createEmptyContext();
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                context.setAuthentication(authToken);
                SecurityContextHolder.setContext(context);
            }
        }
        filterChain.doFilter(request, response);
    }
}

7.自定义权限异常返回

实现AuthenticationEntryPoint和AccessDeniedHandler,通过response写会统一的异常返回。

@Component
public class CustomerAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        ServletOutputStream outputStream = response.getOutputStream();
        ObjectMapper objectMapper = new ObjectMapper();
        outputStream.write(objectMapper.writeValueAsString(ResponseResult.fail(ErrorCode.ILLEGAL_AUTHENTICATE)).getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}


@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        response.setContentType("application/json;charset=utf-8");
        ServletOutputStream outputStream = response.getOutputStream();
        ObjectMapper objectMapper = new ObjectMapper();
        outputStream.write(objectMapper.writeValueAsString(ResponseResult.fail(ErrorCode.ILLEGAL_AUTHENTICATE)).getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

在SecurityConfiguration中配置异常处理

http.exceptionHandling(configurer -> {
      configurer.authenticationEntryPoint(restAuthenticationEntryPoint);
      configurer.accessDeniedHandler(customerAccessDeniedHandler);
});

8.基于角色,更细粒度的控制每个接口的权限

通过在每个接口,配置@PreAuthorize装饰器实现,如

@PreAuthorize("@ss.hasPermi('sysadmin,company_admin,project_admin,worker')")

具体代码就不附上了。

参考

源码:https://github.com/buingoctruong/springboot3-springsecurity6-jwt

视频:2024最新SpringSecurity6安全框架教程-Spring Security+JWT实现项目级前端分离认证_哔哩哔哩_bilibili


http://www.kler.cn/a/582171.html

相关文章:

  • 牛客周赛:84:B:JAVA
  • 亚信安全发布第七期《勒索家族和勒索事件监控报告》
  • Vue3实战学习(Element-Plus常用组件的使用(轮播图、日期时间选择器、表格、分页组件、对话框)(超详细))(下)(6)
  • 设计模式之工厂模式:原理、实现与应用
  • 模拟面试题:系统设计
  • Spring(九)AOP-底层原理与通知方法的理解
  • SQL Server 与 MySQL 的库同步工具推荐
  • 《React 属性与状态江湖:从验证到表单受控的实战探险》
  • 【从零开始学习计算机科学】计算机体系结构(二)指令级并行(ILP)
  • 医疗AI测试实战:如何确保人工智能安全赋能医疗行业?
  • 【C#学习笔记01】从程序框架到函数调用
  • 【AI大模型智能应用】Deepseek生成测试用例
  • 2025,以SBOM为基础的云原生应用安全治理
  • Linux学习(十五)(故障排除(ICMP,Ping,Traceroute,网络统计,数据包分析))
  • 不同AI生成的PHP版雪花算法
  • 装饰器模式的C++实现示例
  • 数据分析绘制随时间顺序变化图加入线性趋势线——numpy库的polyfit计算一次多项式拟合
  • 揭开AI-OPS 的神秘面纱 第五讲 AI 模型服务层(开源方向)
  • electron+vue+webview内嵌网页并注入js
  • 【实战ES】实战 Elasticsearch:快速上手与深度实践-6.1.1RBAC角色权限设计