SpringBoot 整合 Spring Security 实现安全认证【SpringBoot系列9】

602 阅读4分钟

SpringCloud 大型系列课程正在制作中,欢迎大家关注与提意见。

程序员每天的CV 与 板砖,也要知其所以然,本系列课程可以帮助初学者学习 SpringBooot 项目开发 与 SpringCloud 微服务系列项目开发

1 项目准备

Spring Security(官网在这里) 是 Spring 社区的一个顶级项目,也是 Spring Boot 官方推荐使用的安全框架。 在这里插入图片描述

本文章实现的是SpringBoot整合Spring Security实现认证校验功能,实现方式有多种,本文章只是其中一种,如有不足,欢迎留言。


首先在项目的 pom.xml 添加依赖如下:

 <dependency>
       <groupId>com.alibaba</groupId>
       <artifactId>fastjson</artifactId>
       <version>2.0.25</version>
   </dependency>

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

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

JWT(JSON Web Token) 用来生成 Token ,JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名.

添加依赖后,启动项目,在浏览器中访问任何一个接口都会出现 登录认证

1 jwt 生成 token 工具

这里就是根据 用户的 username + 密钥来生成token ,然后解密 token 等等,在 Spring Security 认证过程中使用。

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;

@Component
@Slf4j
public class JWTGenerator {
    //密钥
    private static String sign ="cuAihCz53DZRjZwbsGcZJ2Ai6At+T142uphtJMsk7iQ=";
    //生成token
    public String generateToken(Authentication authentication) {
        //用户的核心标识
        String username = authentication.getName();
        // 过期时间 - 30分钟
        Date expireDate = new Date(System.currentTimeMillis() + 30 * 60 * 1000);
        String token = Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(expireDate)
                .signWith(generalKeyByDecoders())  //设置token加密方式和密
                .compact();
        return token;
    }

    public static SecretKey generalKeyByDecoders() {
        return Keys.hmacShaKeyFor(Decoders.BASE64.decode(sign));
    }

    /**
     * 解密token
     * @param token
     * @return
     */
    public String getUsernameFromJWT(String token) {
        JwtParserBuilder builder = Jwts.parserBuilder();
        Jws<Claims> claimsJws = builder
                .setSigningKey(generalKeyByDecoders())
                .build()
                .parseClaimsJws(token);
        return claimsJws.getBody().getSubject();
    }

    /**
     * 校验token
     * @param token
     * @return
     */
    public boolean validateToken(String token) {
        log.error("验证 token  {}", token);
        try {
            JwtParserBuilder builder = Jwts.parserBuilder();

            Jws<Claims> claimsJws = builder
                    .setSigningKey(generalKeyByDecoders())
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (ExpiredJwtException e) {
            Claims claims = e.getClaims();
            // 检查token
            throw new BadCredentialsException("TOKEN已过期,请重新登录!");
        } catch (AuthenticationException e) {
            throw new AuthenticationCredentialsNotFoundException("JWT was expired or incorrect");
        } catch (Exception ex) {
            log.error("token认证失败 {}", ex.getMessage());
            throw new AuthenticationCredentialsNotFoundException("JWT was expired or incorrect");
        }
    }
}

2 登录认证 Controller 定义

@Slf4j
@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private JWTGenerator jwtGenerator;

    @PostMapping("login")
    public R login(@RequestBody LoginRequest loginDto){
        log.info("登录认证开始 {}",loginDto.toString());
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginDto.getUserName(),
                        loginDto.getPassword()));
        // 认证成功存储认证信息到上下文
        SecurityContextHolder.getContext().setAuthentication(authentication);
        log.info("登录认证完成 {}",loginDto.toString());
        String token = jwtGenerator.generateToken(authentication);
        log.info("登录认证生成 token {}",token);
        return R.okData(token);
    }
 }
  • JWTGenerator 第一步中定义的 token 生成工具,在登录校验完成时,生成token.

  • AuthenticationManager 只关注认证成功与否而并不关心具体的认证方式,如果验证成功,则返回完全填充的Authentication对象(包括授予的权限)。

import lombok.Data;
import lombok.ToString;

import java.io.Serializable;
@Data
@ToString
public class LoginRequest implements Serializable {
    private String userName ;
    private String password;
}

3 核心配置 SecurityConfig

SecurityConfig 用来配置 Spring Security 的拦截策略以及认证策略等等

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    //自定义异常认证处理
    private JwtAuthEntryPoint authEntryPoint;
    //自定义授权异常处理
    private MyAccessDeniedHandler myAccessDeniedHandler;

    @Autowired
    public SecurityConfig(JwtAuthEntryPoint authEntryPoint, MyAccessDeniedHandler myAccessDeniedHandler) {
        this.authEntryPoint = authEntryPoint;
        this.myAccessDeniedHandler = myAccessDeniedHandler;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler)
                .authenticationEntryPoint(authEntryPoint)
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                .authorizeRequests()
                //放行静态资源文件夹(路径要具体情况具体分析)
                .antMatchers(
                        "/api/auth/**",
                        "/css/**", "/js/**", "/image/**",
                        "/app/**",
                        "/swagger/**",
                        "/swagger-ui.html",
                        "/app/**",
                        "/swagger-resources/**",
                        "/v2/**",
                        "/webjars/**").permitAll()

                .anyRequest().authenticated()
                .and()
                .httpBasic();
        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

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

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

 // 自定义 认证过滤器
    @Bean
    public JWTAuthenticationFilter jwtAuthenticationFilter() {
        return new JWTAuthenticationFilter();
    }
}
3.1 JwtAuthEntryPoint 自定义的认证失败的回调处理
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
@Slf4j
public class JwtAuthEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        String message = authException.getMessage();
        log.error("token 拦截 {}",message);
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/javascript;charset=utf-8");
        Map<String,Object> map = new HashMap<>();
        map.put("code",403);
        map.put("message","您未登录,没有访问权限");
        response.getWriter().print(JSONObject.toJSONString(map));
    }
}
3.2 MyAccessDeniedHandler 自定义的授权失败的回调处理
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 授权异常
 */
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException, IOException {
        response.setStatus(403);
        response.getWriter().write("Forbidden:" + accessDeniedException.getMessage());
    }
}

4 核心过滤器 JWTAuthenticationFilter

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JWTAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JWTGenerator tokenGenerator;
    @Autowired
    private CustomUserDetailsService customUserDetailsService;


    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        //获取请求头中的 token 信息
        String token = getJWTFromRequest(request);
        //校验token
        if(StringUtils.hasText(token) && tokenGenerator.validateToken(token)) {
            //解析 token 中的用户信息 (用户的唯一标识 )
            String username = tokenGenerator.getUsernameFromJWT(token);

            UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null,
                    userDetails.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        filterChain.doFilter(request, response);
    }

    /**
     * 就是校验请求头的一种格式 可以随便定义
     * 只要可以解析 就可以
     * @param request
     * @return
     */
    private String getJWTFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7, bearerToken.length());
        }
        return null;
    }

5 CustomUserDetailsService 用户校验实现

@Service
public class CustomUserDetailsService  implements UserDetailsService {

    private UserService userService;

    @Autowired
    public CustomUserDetailsService(UserService userService) {
        this.userService = userService;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserInfo user = userService.getByUsername(username);
        if(user==null){
            throw new  UsernameNotFoundException("Username not found");
        }
        User user1 = new User(user.getUserName(), user.getPassword(), mapRolesToAuthorities(user.getRoles()));
        return user1;
    }

    private Collection<GrantedAuthority> mapRolesToAuthorities(List<Role> roles) {
        return roles.stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList());
    }
}

这里使用到的 UserService 就是项目中查询用户信息的服务。 然后使用 postman 来访问接口 在这里插入图片描述

然后调用 登录接口生成 token

在这里插入图片描述 然后在访问其他接口里放入请求头信息 在这里插入图片描述

项目源码在这里 :gitee.com/android.lon… 有兴趣可以关注一下公众号:biglead

本文正在参加「金石计划」