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

文章目录
  • 前言
  • 1 技术简介
  • 2 项目构建
  • 3 项目配置
  • * 3.1 鉴权配置
    
    • 3.2 登录配置
    • 3.3 Token如何生成
    • 3.4 注册和登录
    • 3.5 请求过滤
    • 3.6 退出登录
  • 4 鉴权
  • * 4.1 controller
    
    • 4.2 配置文件

前言

Spring Security已经成为java后台权限校验的第一选择.今天就通过读代码的方式带大家深入了解一下Security,本文主要是基于开源项目[spring-
boot-3-jwt-security](https://github.com/ali-bouali/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配置类的部分代码

![在这里插入图片描述](https://img-
blog.csdnimg.cn/aaf04e20ba4d421290155cb90f7a0c9c.png#pic_center)

学习计划安排


我一共划分了六个阶段,但并不是说你得学完全部才能上手工作,对于一些初级岗位,学到第三四个阶段就足矣~

这里我整合并且整理成了一份【282G】的网络安全从零基础入门到进阶资料包,需要的小伙伴可以扫描下方CSDN官方合作二维码免费领取哦,无偿分享!!!

如果你对网络安全入门感兴趣,那么你需要的话可以

点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!

①网络安全学习路线
②上百份渗透测试电子书
③安全攻防357页笔记
④50份安全攻防面试指南
⑤安全红队渗透工具包
⑥HW护网行动经验总结
⑦100个漏洞实战案例
⑧安全大厂内部视频资源
⑨历年CTF夺旗赛题解析

Spring BootSpring Security可以很好地结合使用来实现RESTful API的认证。而JWT(JSON Web Token)是一种用于认证和授权的安全传输方式。 要在Spring Boot中实现JWT认证,可以遵循以下步骤: 1. 添加依赖:在`pom.xml`文件中添加以下依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> ``` 2. 创建JWT工具类:创建一个JWT工具类来生成和解析JWT。可以使用JJWT库来简化这个过程。 3. 创建认证过滤器:创建一个继承自`OncePerRequestFilter`的认证过滤器,在该过滤器中检查请求中的JWT,并进行认证。 4. 配置Spring Security:将认证过滤器添加到Spring Security的配置中,以便在每个请求到达之前进行JWT认证。 5. 创建登录接口:创建一个登录接口,用于验证用户的身份并生成JWT。 这是一个简单的示例代码,说明如何在Spring Boot中实现JWT认证: ```java // JWT工具类 public class JwtUtils { private static final String SECRET_KEY = "your-secret-key"; private static final long EXPIRATION_TIME = 864_000_000; // 10天 public static String generateToken(Authentication authentication) { UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); Date expirationDate = new Date(System.currentTimeMillis() + EXPIRATION_TIME); return Jwts.builder() .setSubject(userPrincipal.getUsername()) .setIssuedAt(new Date()) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS512, SECRET_KEY) .compact(); } public static String getUsernameFromToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody() .getSubject(); } public static boolean validateToken(String token) { try { Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token); return true; } catch (SignatureException | MalformedJwtException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException e) { return false; } } } // 认证过滤器 public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtUtils jwtUtils; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authorizationHeader = request.getHeader("Authorization"); if (StringUtils.hasText(authorizationHeader) && authorizationHeader.startsWith("Bearer ")) { String token = authorizationHeader.substring(7); if (jwtUtils.validateToken(token)) { String username = jwtUtils.getUsernameFromToken(token); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } } // Spring Security配置类 @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated() .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } } // 登录接口 @RestController public class AuthController { @Autowired private AuthenticationManager authenticationManager; @PostMapping("/login") public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()) ); SecurityContextHolder.getContext().setAuthentication(authentication); String token = JwtUtils.generateToken(authentication); return ResponseEntity.ok(new JwtResponse(token)); } } // 登录请求DTO public class LoginRequest { private String username; private String password; // getters and setters } // JWT响应DTO public class JwtResponse { private String token; // constructor and getter } // 用户详情实现类 @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username)); return UserDetailsImpl.build(user); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值