Spring Boot 3 + JWT + Security 联手打造安全帝国:一篇文章让你掌握未来!

这篇具有很好参考价值的文章主要介绍了Spring Boot 3 + JWT + Security 联手打造安全帝国:一篇文章让你掌握未来!。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

Spring Security已经成为java后台权限校验的第一选择.今天就通过读代码的方式带大家深入了解一下Security,本文主要是基于开源项目spring-boot-3-jwt-security来讲解Spring Security + JWT(Json Web Token).实现用户鉴权,以及权限校验.
所有代码基于jdk17+构建.现在让我们开始吧!

1 技术简介

  1. Springboot 3.0
  2. Spring Security
  3. Json Web Token(JWT)
  4. BCrypt
  5. Maven

2 项目构建

  1. 项目使用postgresql数据库来存储用户信息以及Token(为啥不用Redis?这个先挖个坑),可以按照自己的想法替换成mysql数据库
  2. 访问数据库使用的是jpa,对于一些简单的sql可以根据方法名自动映射,还是很方便的.没使用过的也没关系.不影响阅读今天的文章,后续可以根据自己的实际需求替换成mybatis-lpus
  3. 本文使用了Lombok来生成固定的模版代码
<parent>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-parent</artifactId>  
    <version>3.0.5</version>  
    <relativePath/> <!-- lookup parent from repository -->  
</parent>  
<groupId>com.alibou</groupId>  
<artifactId>security</artifactId>  
<version>0.0.1-SNAPSHOT</version>  
<name>security</name>  
<description>Demo project for Spring Boot</description>  
<properties>  
    <java.version>17</java.version>  
</properties>  
<dependencies>  
    <!-- jpa -->
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-data-jpa</artifactId>  
    </dependency>  
    <!-- spring security 安全框架 -->
    <dependency>          
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-security</artifactId>  
    </dependency>  
    <!-- web 依赖 -->
    <dependency> 
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-web</artifactId>  
    </dependency>  
    <!-- 数据库 -->
    <dependency>  
        <groupId>org.postgresql</groupId>  
        <artifactId>postgresql</artifactId>  
        <scope>runtime</scope>  
    </dependency>
    <!-- lombok -->
    <dependency>  
        <groupId>org.projectlombok</groupId>  
        <artifactId>lombok</artifactId>  
        <optional>true</optional>  
    </dependency> 
    <!-- JWT -->
    <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>
    
    <!-- doc 这个不需要的可以去掉 -->
    <dependency>  
        <groupId>org.springdoc</groupId>  
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>  
        <version>2.1.0</version>  
    </dependency>
    <!-- 校验 -->
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-validation</artifactId>  
    </dependency>  

    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-test</artifactId>  
        <scope>test</scope>  
    </dependency>  
    <dependency>  
        <groupId>org.springframework.security</groupId>  
        <artifactId>spring-security-test</artifactId>  
        <scope>test</scope>  
    </dependency>  
</dependencies>  
  
<build>  
    <plugins>  
        <plugin>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-maven-plugin</artifactId>  
            <configuration>  
                <excludes>  
                    <exclude>  
                        <groupId>org.projectlombok</groupId>  
                        <artifactId>lombok</artifactId>  
                    </exclude>  
                </excludes>  
            </configuration>  
        </plugin>  
    </plugins>  
</build>

3 项目配置

3.1 鉴权配置

  1. 当项目引入Security依赖后,启动项目会生成一个随机的密码,当我们要访问资源的时候需要使用这个密码登录后才能使用.这会影响我们很多功能的正常使用,比如万恶的swagger.下面我们来详细了解如何配置我们需要鉴权的路径,以及需要放行的路径
@Configuration  
@EnableWebSecurity  
@RequiredArgsConstructor  
@EnableMethodSecurity  
public class SecurityConfiguration {  
  
private final JwtAuthenticationFilter jwtAuthFilter;  
private final AuthenticationProvider authenticationProvider;  
private final LogoutHandler logoutHandler;  
  
@Bean  
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {  
    http  
    .csrf()  
    .disable() //关闭csrf(跨域)  
    .authorizeHttpRequests()  
    //配置需要放行的路径  
    .requestMatchers(  
    "/api/v1/auth/**",  
    "/v2/api-docs",  
    "/v3/api-docs",  
    "/v3/api-docs/**",  
    "/swagger-resources",  
    "/swagger-resources/**",  
    "/configuration/ui",  
    "/configuration/security",  
    "/swagger-ui/**",  
    "/webjars/**",  
    "/swagger-ui.html"  
    )  
    .permitAll() //放行上述的所有路径  
  
  
    /*  
    * 权限校验(需要登录的用户有指定的权限才可以)  
    * requestMatchers: 指定需要拦截的路径  
    * hasAnyAuthority: 指定需要的权限  
    */  
    .requestMatchers("/api/v1/management/**").hasAnyRole(ADMIN.name(), MANAGER.name())  
    .requestMatchers(GET, "/api/v1/management/**").hasAnyAuthority(ADMIN_READ.name(), MANAGER_READ.name())  
    .requestMatchers(POST, "/api/v1/management/**").hasAnyAuthority(ADMIN_CREATE.name(), MANAGER_CREATE.name())  
    .requestMatchers(PUT, "/api/v1/management/**").hasAnyAuthority(ADMIN_UPDATE.name(), MANAGER_UPDATE.name())  
    .requestMatchers(DELETE, "/api/v1/management/**").hasAnyAuthority(ADMIN_DELETE.name(), MANAGER_DELETE.name())  
    .anyRequest()  
    .authenticated() //设置所有的请求都需要验证  
    .and()  
    .sessionManagement()  
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS) //使用无状态Session  
    .and()  
    .authenticationProvider(authenticationProvider)  
    //添加jwt过滤器  
    .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)  
    //设置logout(当调用这个接口的时候, 会调用logoutHandler的logout方法)  
    .logout()  
    .logoutUrl("/api/v1/auth/logout")  
    .addLogoutHandler(logoutHandler)  
    .logoutSuccessHandler((request, response,authentication) -> SecurityContextHolder.clearContext())  
    ;  

    return http.build();  
    }  
}
  1. 上述代码主要实现了四块功能分别是:
    • 放行不需要鉴权的路径(注册&登录,swagger)
    • 配置访问特定的接口用户需要的权限.(比如想要删除用户必须要有删除用户的权限)
    • 添加前置过滤器,用来从Token中判断用户是否合法和获取用户权限: jwtAuthFilter
    • 配置退出登录的Handler,以及监听的路径.当访问这个路径的时候会自动调用logoutHandler中的方法

3.2 登录配置

上面说到了权限和token校验,我们先来了解一下登录的逻辑是什么样的.在security中需要一个UserDetails类来定义用户账户的行为.这个是用户鉴权的关键.主要有账户,密码,权限,用户状态等等.在下面代码中有详细的注释

@Data  
@Builder  
@NoArgsConstructor  
@AllArgsConstructor  
@Entity  
@Table(name = "_user")  
public class User implements UserDetails {  
  
    @Id  
    @GeneratedValue  
    private Integer id; //主键ID  
    private String firstname; //名字  
    private String lastname; //姓氏  
    private String email; //邮箱  
    private String password; //密码  

    /**  
    * 角色枚举  
    */  
    @Enumerated(EnumType.STRING)  
    private Role role;  

    /**  
    * 用户关联的Token  
    * 这里面使用了jpa的一对多映射  
    */  
    @OneToMany(mappedBy = "user")  
    private List<Token> tokens;  

    /**  
    * 获取用户的权限  
    * 这里是根据角色枚举的权限来获取的(静态的而非从数据库动态读取)  
    * @return 用户权限列表  
    */  
    @Override  
    public Collection<? extends GrantedAuthority> getAuthorities() {  
        return role.getAuthorities();  
    }  

    /**  
    * 获取用户密码  
    * 主要是用来指定你的password字段  
    * @return 用户密码  
    */  
    @Override  
    public String getPassword() {  
        return password;  
    }  

    /**  
    * 获取用户账号  
    * 这里使用email做为账号  
    * @return 用户账号  
    */  
    @Override  
    public String getUsername() {  
        return email;  
    }  

    /**  
    * 账号是否未过期,下面的这个几个方法都是用来指定账号的状态的,因为该项目是一个Demo,所以这里全部返回true  
    * @return true 未过期  
    */  
    @Override  
    public boolean isAccountNonExpired() {  
        return true;  
    }  

    /**  
    * 账号是否未锁定  
    * @return true 未锁定  
    */  
    @Override  
    public boolean isAccountNonLocked() {  
        return true;  
    }  

    /**  
    * 密码是否未过期  
    * @return true 未过期  
    */  
    @Override  
    public boolean isCredentialsNonExpired() {  
        return true;  
    }  

    /**  
    * 账号是否激活  
    * @return true 已激活  
    */  
    @Override  
    public boolean isEnabled() {  
        return true;  
    }  
}

在了解用户实体之后,我们来看一下是怎么来进行登录配置的.如何使用securty来帮我们管理用户密码的校验.下面我们来看一下security的整体配置

@Configuration  
@RequiredArgsConstructor  
public class ApplicationConfig {  
  
    /**  
    * 访问用户数据表  
    */  
    private final UserRepository repository;  

    /**  
    * 获取用户详情Bean  
    * 根据email查询是否存在用户,如果不存在throw用户未找到异常  
    */  
    @Bean  
    public UserDetailsService userDetailsService() {  
        //调用repository的findByEmail方法,来获取用户信息,如果存在则返回,如果不存在则抛出异常  
        return username -> repository.findByEmail(username)  
        //这里使用的Option的orElseThrow方法,如果存在则返回,如果不存在则抛出异常  
        .orElseThrow(() -> new UsernameNotFoundException("User not found"));  
    }  

    /**  
    * 身份验证Bean  
    * 传入获取用户信息的bean & 密码加密器  
    * 可以回看一下SecurityConfiguration中 AuthenticationProvider的配置,使用的就是这里注入到容器中的Bean  
    * 这个bean 主要是用于用户登录时的身份验证,当我们登录的时候security会帮我们调用这个bean的authenticate方法  
    */  
    @Bean  
    public AuthenticationProvider authenticationProvider() {  
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();  
        //设置获取用户信息的bean  
        authProvider.setUserDetailsService(userDetailsService());  
        //设置密码加密器  
        authProvider.setPasswordEncoder(passwordEncoder());  
        return authProvider;  
    }  

    /**  
    * 身份验证管理器  
    */  
    @Bean  
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {  
        return config.getAuthenticationManager();  
    }  

    /**  
    * 密码加密器  
    * 主要是用来指定数据库中存储密码的加密方式,保证密码非明文存储  
    * 当security需要进行密码校验时,会把请求传进来的密码进行加密,然后和数据库中的密码进行比对  
    */  
    @Bean  
    public PasswordEncoder passwordEncoder() {  
        return new BCryptPasswordEncoder();  
    }  
  
}

上述代码主要做了两件事:

  • 指定我们如何从数据库中根据用户账号获取用户信息
  • 指定用户密码的加密器passwordEncoder

现在大家可能会存在一个疑问,security怎么知道User实体中那个字段是我的账户,那个字段是我的密码?
不知道大家是否记得UserDetails类,也就是我们的User类.其中有两个方法getPassword & getUsername.这两个方法返回的就是账号和密码.User类中的还有几个其他的方法,可以根据我们实际的业务需求来对账号进行禁用等操作.

3.3 Token如何生成

token的生成主要是使用工具包来实现,在本项目中Token中主要存储用户信息 & 用户权限,下面我们先看一下token工具包的代码.主要包括为: 生成token,从token中获取信息,以及验证token

@Service  
public class JwtService {  
  
    /**  
    * 加密盐值  
    */  
    @Value("${application.security.jwt.secret-key}")  
    private String secretKey;  

    /**  
    * Token失效时间  
    */  
    @Value("${application.security.jwt.expiration}")  
    private long jwtExpiration;  

    /**  
    * Token刷新时间  
    */  
    @Value("${application.security.jwt.refresh-token.expiration}")  
    private long refreshExpiration;  

    /**  
    * 从Token中获取Username  
    * @param token Token  
    * @return String  
    */  
    public String extractUsername(String token) {  
        return extractClaim(token, Claims::getSubject);  
    }  

    /**  
    * 从Token中回去数据,根据传入不同的Function返回不同的数据  
    * eg: String extractUsername(String token)  
    */  
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {  
        final Claims claims = extractAllClaims(token);  
        return claimsResolver.apply(claims);  
    }  

    /**  
    * 生成Token无额外信息  
    */  
    public String generateToken(UserDetails userDetails) {  
        return generateToken(new HashMap<>(), userDetails);  
    }  

    /**  
    * 生成Token,有额外信息  
    * @param extraClaims 额外的数据  
    * @param userDetails 用户信息  
    * @return String  
    */  
    public String generateToken(  
    Map<String, Object> extraClaims,  
    UserDetails userDetails  
    ) {  
        return buildToken(extraClaims, userDetails, jwtExpiration);  
    }  

    /**  
    * 生成刷新用的Token  
    * @param userDetails 用户信息  
    * @return String  
    */  
    public String generateRefreshToken(  
    UserDetails userDetails  
    ) {  
        return buildToken(new HashMap<>(), userDetails, refreshExpiration);  
    }  

    /**  
    * 构建Token方法  
    * @param extraClaims 额外信息  
    * @param userDetails //用户信息  
    * @param expiration //失效时间  
    * @return String  
    */  
    private String buildToken(  
        Map<String, Object> extraClaims,  
        UserDetails userDetails,  
        long expiration  
        ) {  
        return Jwts  
        .builder()  
        .setClaims(extraClaims) //body  
        .setSubject(userDetails.getUsername()) //主题数据  
        .setIssuedAt(new Date(System.currentTimeMillis())) //设置发布时间  
        .setExpiration(new Date(System.currentTimeMillis() + expiration)) //设置过期时间  
        .signWith(getSignInKey(), SignatureAlgorithm.HS256) //设置摘要算法  
        .compact();  
    }  

    /**  
    * 验证Token是否有效  
    * @param token Token  
    * @param userDetails 用户信息  
    * @return boolean  
    */  
    public boolean isTokenValid(String token, UserDetails userDetails) {  
        final String username = extractUsername(token);  
        return (username.equals(userDetails.getUsername())) && !isTokenExpired(token); 
    }  

    /**  
    * 判断Token是否过去  
    */  
    private boolean isTokenExpired(String token) {  
        return extractExpiration(token).before(new Date());  
    }  

    /**  
    * 从Token中获取失效时间  
    */  
    private Date extractExpiration(String token) {  
        //通用方法,传入一个Function,返回一个T  
        return extractClaim(token, Claims::getExpiration);  
    }  

    /**  
    * 从Token中获取所有数据  
    */  
    private Claims extractAllClaims(String token) {  
        return Jwts  
        .parserBuilder()  
        .setSigningKey(getSignInKey())  
        .build()  
        .parseClaimsJws(token)  
        .getBody();  
    }  

    /**  
    * 获取签名Key  
    * Token 加密解密使用  
    */  
    private Key getSignInKey() {  
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);  
        return Keys.hmacShaKeyFor(keyBytes);  
    }  
}

3.4 注册和登录

token的生成已经看过了,下面该进入最关键的环节了.用户注册 & 用户登录

  1. 用户注册: 接收到用户传递过来的信息,在数据库中生成用户信息(密码会通过passwordEncoder进行加密).用户信息保存成功后,会根据用户信息创建一个鉴权token和一个refreshToken
  2. 用户登录: 获取到用户传递的账号密码后,会创建一个UsernamePasswordAuthenticationToken对象.然后通过authenticationManagerauthenticate方法进行校验,如果出现错误会根据错误的不同抛出不同的异常.在实际开发中可以通过捕获的异常类型不同来创建响应提示.
@RestController  
@RequestMapping("/api/v1/auth")  
@RequiredArgsConstructor  
public class AuthenticationController {  

    private final AuthenticationService service;  

    /**  
    * 注册方法  
    * @param request 请求体  
    * @return ResponseEntity  
    */  
    @PostMapping("/register")  
    public ResponseEntity<AuthenticationResponse> register(  
    @RequestBody RegisterRequest request  
    ) {  
        return ResponseEntity.ok(service.register(request));  
    }  

    /**  
    * 鉴权(登录方法)  
    * @param request 请求体  
    * @return ResponseEntity  
    */  
    @PostMapping("/authenticate")  
    public ResponseEntity<AuthenticationResponse> authenticate(  
    @RequestBody AuthenticationRequest request  
    ) {  
        return ResponseEntity.ok(service.authenticate(request));  
    }  

    /**  
    * 刷新token  
    * @param request 请求体  
    * @param response 响应体  
    * @throws IOException 异常  
    */  
    @PostMapping("/refresh-token")  
    public void refreshToken(  
    HttpServletRequest request,  
    HttpServletResponse response  
    ) throws IOException {  
        service.refreshToken(request, response);  
    }  
}

可以看出来controller中的方法都是对service方法的调用,我们现在看一下service中的代码

@Service  
@RequiredArgsConstructor  
public class AuthenticationService {  
  
    private final UserRepository repository; //访问user数据库  
    private final TokenRepository tokenRepository; //访问token数据库  
    private final PasswordEncoder passwordEncoder; //密码加密器  
    private final JwtService jwtService; //JWT 相关方法  
    private final AuthenticationManager authenticationManager; //Spring Security 认证管理器  

    /**  
    * 注册方法  
    * @param request 请求体  
    * @return AuthenticationResponse(自己封装的响应结构)  
    */  
    public AuthenticationResponse register(RegisterRequest request) {  
    //构建用户信息  
        var user = User.builder()  
        .firstname(request.getFirstname())  
        .lastname(request.getLastname())  
        .email(request.getEmail())  
        .password(passwordEncoder.encode(request.getPassword()))  
        .role(request.getRole())  
        .build();  

        //将用户信息保存到数据库  
        var savedUser = repository.save(user);  
        //通过JWT方法生成Token  
        var jwtToken = jwtService.generateToken(user);  
        //生成RefreshToken(刷新Token使用)  
        var refreshToken = jwtService.generateRefreshToken(user);  
        //将Token保存到数据库  
        saveUserToken(savedUser, jwtToken);  
        //返回响应体  
        return AuthenticationResponse.builder()  
        .accessToken(jwtToken)  
        .refreshToken(refreshToken)  
        .build();  
    }  

    /**  
    * 鉴权(登录)方法  
    * @param request 请求体  
    * @return AuthenticationResponse(自己封装的响应结构)  
    */  
    public AuthenticationResponse authenticate(AuthenticationRequest request) {  
        //通过Spring Security 认证管理器进行认证  
        //如果认证失败会抛出异常 eg:BadCredentialsException 密码错误 UsernameNotFoundException 用户不存在  
        authenticationManager.authenticate(  
        new UsernamePasswordAuthenticationToken(  
        request.getEmail(),  
        request.getPassword()  
        )  
        );  
        //通过邮箱查询用户信息,当前项目email就是账号  
        var user = repository.findByEmail(request.getEmail())  
        .orElseThrow();  
        //通过JWT方法生成Token  
        var jwtToken = jwtService.generateToken(user);  
        //生成RefreshToken(刷新Token使用)  
        var refreshToken = jwtService.generateRefreshToken(user);  
        //将之前所有的Token变成失效状态  
        revokeAllUserTokens(user);  
        //保存新的Token到数据库  
        saveUserToken(user, jwtToken);  
        //封装响应体  
        return AuthenticationResponse.builder()  
        .accessToken(jwtToken)  
        .refreshToken(refreshToken)  
        .build();  
    }  

    /**  
    * 保存用户Token方法  
    * 构建Token实体后保存到数据库  
    * @param user 用户信息  
    * @param jwtToken Token  
    */  
    private void saveUserToken(User user, String jwtToken) {  
        var token = Token.builder()  
        .user(user)  
        .token(jwtToken)  
        .tokenType(TokenType.BEARER)  
        .expired(false)  
        .revoked(false)  
        .build();  
        tokenRepository.save(token);  
    }  

    /**  
    * 将用户所有Token变成失效状态  
    * @param user 用户信息  
    */  
    private void revokeAllUserTokens(User user) {  
        //获取用户所有有效的token  
        var validUserTokens = tokenRepository.findAllValidTokenByUser(user.getId());  
        if (validUserTokens.isEmpty()){  
        return;  
        }  
        //如果存在还为失效的token,将token置为失效  
        validUserTokens.forEach(token -> {  
        token.setExpired(true);  
        token.setRevoked(true);  
        });  
        tokenRepository.saveAll(validUserTokens);  
    }  

    /**  
    * 刷新token方法  
    * @param request 请求体  
    * @param response 响应体  
    * @throws IOException 抛出IO异常  
    */  
    public void refreshToken(  
    HttpServletRequest request,  
    HttpServletResponse response  
    ) throws IOException {  
        //从请求头中获取中获取鉴权信息 AUTHORIZATION  
        final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);  
        final String refreshToken;  
        final String userEmail;  
        //如果鉴权信息为空或者不是以Bearer 开头的,直接返回  
        if (authHeader == null ||!authHeader.startsWith("Bearer ")) {  
            return;  
        }  
        //从鉴权信息中获取RefreshToken  
        refreshToken = authHeader.substring(7);  
        //从RefreshToken中获取用户信息  
        userEmail = jwtService.extractUsername(refreshToken);  
        if (userEmail != null) {  
            //根据用户信息查询用户,如果用户不存在抛出异常  
            var user = this.repository.findByEmail(userEmail)  
            .orElseThrow();  

            //验证Token是否有效  
            if (jwtService.isTokenValid(refreshToken, user)) {  
                //生成新的Token  
                var accessToken = jwtService.generateToken(user);  
                revokeAllUserTokens(user);  
                saveUserToken(user, accessToken);  
                //生成新的Token和RefreshToken并通过响应体返回  
                var authResponse = AuthenticationResponse.builder()  
                .accessToken(accessToken)  
                .refreshToken(refreshToken)  
                .build();  
                new ObjectMapper().writeValue(response.getOutputStream(), authResponse);  
            }  
        }  
    }  
}

上述代码主要说明了,注册 & 登录后返回token的流程,当前项目中由于token & refreshToken有效期较长所以选择了将token保存到数据库(个人观点!!!).可以根据自己业务的实际需求来决定是否需要保存到redis

3.5 请求过滤

请求过滤主要是在每次请求的时候动态解析token来获取用户信息以及权限,来保证请求资源的安全性.防止越权访问等.

@Component  
@RequiredArgsConstructor  
public class JwtAuthenticationFilter extends OncePerRequestFilter {  
  
    private final JwtService jwtService;  
    private final UserDetailsService userDetailsService;  
    private final TokenRepository tokenRepository;  

    @Override  
    protected void doFilterInternal(  
    @NonNull HttpServletRequest request,  
    @NonNull HttpServletResponse response,  
    @NonNull FilterChain filterChain  
    ) throws ServletException, IOException {  
        //判断请求是否为登录请求,如果是登录请求则不进行处理  
        if (request.getServletPath().contains("/api/v1/auth")) {  
            filterChain.doFilter(request, response);  
            return;  
        }  
        //从请求头中获取鉴权authHeader  
        final String authHeader = request.getHeader("Authorization");  
        final String jwt;  
        final String userEmail;  

        //如果不存在Token或者Token不已Bearer开头,则不进行处理  
        if (authHeader == null ||!authHeader.startsWith("Bearer ")) {  
            filterChain.doFilter(request, response);  
            return;  
        }  
        //从authHeader中截取出Token信息  
        jwt = authHeader.substring(7);  
        //从Token中获取userEmail(账户)  
        userEmail = jwtService.extractUsername(jwt);  
        //SecurityContextHolder 中的 Authentication 为空时,才进行处理  
        if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {  
            //获取用户信息  
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);  

            //从数据库中查询Token并判断Token状态是否正常  
            var isTokenValid = tokenRepository.findByToken(jwt)  
                .map(t -> !t.isExpired() && !t.isRevoked())  
                .orElse(false);  

            //如果Token有效,并且Token状态正常,将用户信息存储到SecurityContextHolder  
            if (jwtService.isTokenValid(jwt, userDetails) && isTokenValid) {  
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(  
                userDetails, //用户信息  
                null,  
                userDetails.getAuthorities() //用户的权限  
                );  
                authToken.setDetails(  
                    new WebAuthenticationDetailsSource().buildDetails(request) //访问信息  
                );  
                //将用户信息以及权限保存到 SecurityContextHolder的上下文中,方便后续使用  
                //eg: 获取当前用户id,获取当前用户权限等等  
                SecurityContextHolder.getContext().setAuthentication(authToken);  
            }  
        }  
        filterChain.doFilter(request, response);  
    }  
}

上述代码主要逻辑为: 从请求头中获取到token.验证token的有效性并解析token中的信息存储到SecurityContextHolder上下文中,方便后续的使用.

3.6 退出登录

登录以及token的校验已经说过了,现在就差一个退出登录了.大家是否还记得我们之前配置过一个退出登录的请求路径: /api/v1/auth/logout.当我们请求请求这个路径的时候,security会帮我们找到对应的LogoutHandler,然后调用logout方法实现退出登录.

@Service  
@RequiredArgsConstructor  
public class LogoutService implements LogoutHandler {  

    private final TokenRepository tokenRepository;  

    @Override  
    public void logout(  
    HttpServletRequest request,  
    HttpServletResponse response,  
    Authentication authentication  
    ) {  
        //从请求头中获取鉴权信息  
        final String authHeader = request.getHeader("Authorization");  
        final String jwt;  
        if (authHeader == null ||!authHeader.startsWith("Bearer ")) {  
        return;  
        }  
        //接续出token  
        jwt = authHeader.substring(7);  
        //从数据库中查询出token信息  
        var storedToken = tokenRepository.findByToken(jwt)  
        .orElse(null);  
        if (storedToken != null) {  
            //设置token过期  
            storedToken.setExpired(true);  
            storedToken.setRevoked(true);  
            tokenRepository.save(storedToken);  
            //清除SecurityContextHolder上下文  
            SecurityContextHolder.clearContext();  
        }  
    }  
}

security帮我们做了很多的事情,我们只需要把token置为失效状态,然后清除掉SecurityContextHolder上下文,就解决了全部的问题

4 鉴权

下面通过几个例子,来讲解两种不同的鉴权配置方式

4.1 controller

@RestController  
@RequestMapping("/api/v1/admin")  
@PreAuthorize("hasRole('ADMIN')") //用户需要ADMIN角色才能访问  
public class AdminController {  
  
    @GetMapping  
    @PreAuthorize("hasAuthority('admin:read')") //用户需要admin:read权限才能访问  
    public String get() {  
        return "GET:: admin controller";  
    }  
    @PostMapping  
    @PreAuthorize("hasAuthority('admin:create')") //用户需要admin:create权限才能访问  
    @Hidden  
    public String post() {  
        return "POST:: admin controller";  
    }  
    @PutMapping  
    @PreAuthorize("hasAuthority('admin:update')")  
    @Hidden  
    public String put() {  
        return "PUT:: admin controller";  
    }  
    @DeleteMapping  
    @PreAuthorize("hasAuthority('admin:delete')")  
    @Hidden  
    public String delete() {  
        return "DELETE:: admin controller";  
    }  
}

4.2 配置文件

下面贴出SecurityConfiguration配置类的部分代码

Spring Boot 3 + JWT + Security 联手打造安全帝国:一篇文章让你掌握未来!,后端,spring boot,安全,后端,java文章来源地址https://www.toymoban.com/news/detail-725891.html

到了这里,关于Spring Boot 3 + JWT + Security 联手打造安全帝国:一篇文章让你掌握未来!的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • Spring Boot安全管理—Spring Security基本配置

    Spring Boot安全管理—Spring Security基本配置

    1.1 创建项目,添加依赖 创建一个Spring Boot Web 项目,然后添加spring-boot-starter-security依赖。 1.2 添加hello接口 在项目中添加一个简单的/hello接口,内容如下: 1.3 启动项目测试 访问/hello接口会自动跳转到登录页面,这个页面有Spring Security提供的。 默认的用户名是user,默认的登

    2024年02月08日
    浏览(6)
  • 【8】Spring Boot 3 集成组件:安全组件 spring security【官网概念篇】

    【8】Spring Boot 3 集成组件:安全组件 spring security【官网概念篇】

    个人主页: 【⭐️个人主页】 需要您的【💖 点赞+关注】支持 💯 📖 本文核心知识点: spring security B 官网Doc : Spring Security Spring Security 是一个框架,它提供 身份验证 、 授权 和针对常见 攻击的保护 。它具有保护命令式和响应式应用程序的一流支持,是保护基于spring的应用

    2024年02月03日
    浏览(13)
  • spring boot中常用的安全框架 Security框架 利用Security框架实现用户登录验证token和用户授权(接口权限控制)

    spring boot中常用的安全框架 Security框架 利用Security框架实现用户登录验证token和用户授权(接口权限控制)

    spring boot中常用的安全框架 Security 和 Shiro 框架 Security 两大核心功能 认证 和 授权 重量级 Shiro 轻量级框架 不限于web 开发 在不使用安全框架的时候 一般我们利用过滤器和 aop自己实现 权限验证 用户登录 Security 实现逻辑 输入用户名和密码 提交 把提交用户名和密码封装对象

    2024年02月06日
    浏览(13)
  • Spring Security 构建基于 JWT 的登录认证

    Spring Security 构建基于 JWT 的登录认证

    一言以蔽之,JWT 可以携带非敏感信息,并具有不可篡改性。可以通过验证是否被篡改,以及读取信息内容,完成网络认证的三个问题:“你是谁”、“你有哪些权限”、“是不是冒充的”。   为了安全,使用它需要采用 Https 协议,并且一定要小心防止用于加密的密钥泄露。

    2024年02月16日
    浏览(8)
  • Spring Security详细讲解(JWT+SpringSecurity登入案例)

    Spring Security详细讲解(JWT+SpringSecurity登入案例)

    1.SpringSecurity SpringSecurity 是一个功能强大且高度可定制的身份验证和访问控制框架 。它是保护基于 Spring 的应用程序的事实上的标准。 SpringSecurity 是一个致力于为 Java 应用程序提供身份验证和授权的框架 。像所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以如何轻

    2024年02月02日
    浏览(11)
  • Java开发 - 单点登录初体验(Spring Security + JWT)

    Java开发 - 单点登录初体验(Spring Security + JWT)

    目录 ​​​​​​​ 前言 为什么要登录 登录的种类 Cookie-Session Cookie-Session-local storage JWT令牌 几种登陆总结  用户身份认证与授权 创建工程 添加依赖 启动项目 Bcrypt算法的工具 创建VO模型类 创建接口文件 创建XML文件 补充配置 添加依赖 添加配置 创建配置类 测试上面的配置

    2024年02月02日
    浏览(12)
  • spring-security -oauth2 整合 JWT

    spring-security -oauth2 整合 JWT

    在这个基础上,进行整合。 spring security oauth2学习 -- 快速入门_本郡主是喵的博客-CSDN博客 先把  reids,common-pools  等依赖删掉。 删掉redis的下相关配置 1.1 导入依赖 1.2 核心代码 创建 jwtTokenConfig.java 在 AuthenticationServer.java 里面新增这些。  运行,启动!  复制这个token去官网解析

    2024年02月09日
    浏览(13)
  • Spring Security OAuth 2.0 资源服务器— JWT

    Spring Security OAuth 2.0 资源服务器— JWT

    目录 一、JWT的最小依赖 二、JWT的最基本配置 1、指定授权服务器 2、初始预期(Startup Expectations) 3、运行时预期(Runtime Expectations) 三、JWT认证是如何工作的 四、直接指定授权服务器 JWK Set Uri 五、提供 audiences 六、覆盖或取代启动自动配置 1、使用jwkSetUri() 2、使用decoder()

    2024年02月05日
    浏览(8)
  • SpringBoot2.3集成Spring Security(二) JWT认证

    SpringBoot2.3集成Spring Security(二) JWT认证

    紧接上文,我们已经完成了 SpringBoot中集成Spring Security,并且用户名帐号和密码都是从数据库中获取。但是这种方式还是不能满足现在的开发需求。 使用JWT的好处: 无状态认证:JWT本身包含了认证信息和声明,服务器不需要在会话中保存任何状态。这样使得应用程序可以更加

    2024年02月11日
    浏览(10)
  • SpringCloud gateway+Spring Security + JWT实现登录和用户权限校验

    SpringCloud gateway+Spring Security + JWT实现登录和用户权限校验

    原本打算将Security模块与gateway模块分开写的,但想到gateway本来就有过滤的作用 ,于是就把gateway和Security结合在一起了,然后结合JWT令牌对用户身份和权限进行校验。 Spring Cloud的网关与传统的SpringMVC不同,gateway是基于Netty容器,采用的webflux技术,所以gateway模块不能引入spri

    2024年02月03日
    浏览(13)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包