前言

Spring Security现已成为java后台权限校验的榜首挑选.今日就经过读代码的办法带咱们深入了解一下Security,本文首要是依据开源项目spring-boot-3-jwt-security来讲解Spring Security + JWT(Json Web Token).完成用户鉴权,以及权限校验. 一切代码依据jdk17+构建.现在让咱们开始吧!

技能简介

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

项目构建

  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>

项目装备

鉴权装备

  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中的办法

登录装备

上面说到了权限和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类中的还有几个其他的办法,能够依据咱们实践的业务需求来对账号进行禁用等操作.

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);  
    }  
}

注册和登录

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

恳求过滤

恳求过滤首要是在每次恳求的时分动态解析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上下文中,便利后续的运用.

退出登录

登录以及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上下文,就处理了悉数的问题

鉴权

下面经过几个例子,来讲解两种不同的鉴权装备办法

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";  
    }  
}

装备文件

下面贴出SecurityConfiguration装备类的部分代码

一文带你了解springboot3+jwt+security的使用