Spring Boot + Spring Security + JWT

Spring Security 流程图

1. 新建Spring Boot 项目

1.1 创建 Controller
@RestController
@RequestMapping(value = "/api")
public class UserController {
    @Autowired
    private UserService userService;
    @GetMapping(value = "/user/{username}")
    public User getUserByUsername(@PathVariable("username") String username) {
        return userService.findUserByUsername(username);
    }
}
1.2 创建 Service 及 Entity
public interface UserService {
    User findUserByUsername(String username);
}

@Slf4j
@Service
public class UserServiceImpl implements UserService {
    @Override
    public User findUserByUsername(String username) {
        log.info("Executing find user by username [{}]", username);
        return new User(username, "123456", "1");
    }
}

@Data
public class User {
    private String username;
    private String password;
    private String authority;
    
    public User() {
    }
    
    public User(User user) {
        this.username = user.getUsername();
        this.password = user.getPassword();
        this.authority = user.getAuthority();
    }
}
1.3 pom 文件
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

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

2. 添加 Spring Security + JWT

Spring Security 验证身份的方式是利用 Filter,再加上 HttpServletRequest 的一些信息进行过滤,基本角色如下:

  • 类 Authentication 保存的是身份认证信息。
  • 类 AuthenticationProvider 提供身份认证途径。
  • 类 AuthenticationManager 保存的 AuthenticationProvider 集合,并调用 AuthenticationProvider 进行身份认证。
2.1 pom 文件
        <!-- spring security 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!-- jwt 依赖 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

        <!-- swagger-ui 依赖 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${swagger-version}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${swagger-version}</version>
        </dependency>
2.3 创建请求实体类
public class LoginRequest {
    private String username;
    private String password;
    
    @JsonCreator
    public LoginRequest(@JsonProperty("username") String username, @JsonProperty("password") String password) {
        this.username = username;
        this.password = password;
    }
    
    public String getUsername() {
        return username;
    }
    
    public String getPassword() {
        return password;
    }
}
2.3 创建认证过滤器

AbstractAuthenticationProcessingFilter 是一个抽象类,主要的功能是身份认证,子类实现 attemptAuthentication 方法提供身份认证的具体实现,根据 HttpServletRequest 等信息进行身份认证,并返回 Authentication 对象、 null、异常,分别表示认证成功返回的身份认证信息、需要其他 Filter 继续进行身份认证、认证失败。

public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {

    private final AuthenticationSuccessHandler successHandler;
    private final AuthenticationFailureHandler failureHandler;
    private final ObjectMapper objectMapper;

    public RestLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler, AuthenticationFailureHandler failureHandler, ObjectMapper mapper) {
        super(defaultProcessUrl);
        this.successHandler = successHandler;
        this.failureHandler = failureHandler;
        this.objectMapper = mapper;
    }

    /**
     * 具体身份认证方法
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        LoginRequest loginRequest;
        try {
            loginRequest = objectMapper.readValue(request.getReader(), LoginRequest.class);
        } catch (Exception e) {
            throw new AuthenticationServiceException("Invalid login request payload");
        }
        if (StringUtils.isEmpty(loginRequest.getUsername()) || StringUtils.isEmpty(loginRequest.getPassword())) {
            throw new AuthenticationServiceException("Username or Password not provided");
        }
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
		// 调用注册到AuthenticationManager中的AuthenticationProvider的认证方法authenticate
        return this.getAuthenticationManager().authenticate(token);
    }

    /**
     * 认证成功回调
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

    /**
     * 认证失败回调
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
	    SecurityContextHolder.clearContext();
        failureHandler.onAuthenticationFailure(request, response, failed);
    }
}
2.4 创建自定义AuthenticationProvider
@Component
public class RestAuthenticationProvider implements AuthenticationProvider {

    private final UserService userService;
    private final BCryptPasswordEncoder encoder;    // 密码加密

    @Autowired
    public RestAuthenticationProvider(final UserService userService, final BCryptPasswordEncoder encoder) {
        this.encoder = encoder;
        this.userService = userService;
    }
    
    /**
     * 身份认证方法
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.notNull(authentication, "No authentication data provided");
        // 用户名
        String username = authentication.getName();
        // 密码
        String password = (String) authentication.getCredentials();
        // 查询用户信息
        User user = userService.getUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found: " + username);
        }
        if (!encoder.matches(password, user.getPassword())) {
            throw new BadCredentialsException("Authentication Failed. Username or Password not valid.");
        }
        SecurityUser securityUser = new SecurityUser(user);
        // 认证成功回调获取安全用户信息是在这里实例化的(UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities))
        return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
    }

    /**
     * 检查authentication的类型是不是这个AuthenticationProvider支持的
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
    }
}
2.5 创建成功和失败返回
/**
 * 认证成功回调
 */
@Component
public class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private final ObjectMapper mapper;
    private final JwtTokenFactory tokenFactory;

    @Autowired
    public RestAuthenticationSuccessHandler(final ObjectMapper mapper, final JwtTokenFactory tokenFactory) {
        this.mapper = mapper;
        this.tokenFactory = tokenFactory;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Map<String, Object> hashMap = new ConcurrentHashMap<>();
        // 获取安全的用户信息
        SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
        // 生成token(具体方法见后面)
        String token = tokenFactory.createAccessJwtToken(securityUser);
        hashMap.put("token", token);
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        mapper.writeValue(response.getWriter(),hashMap);
    }
}

/**
 * 认证失败回调
 */
@Component
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Autowired
    private ObjectMapper mapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
    	// 暂时未处理失败回调
        Map<String, Object> hashMap = new ConcurrentHashMap<>();
        hashMap.put("code", 401);
        hashMap.put("status", false);
        hashMap.put("message", "User not logged in");
        hashMap.put("timestamp", new Date());
        mapper.writeValue(response.getWriter(), hashMap);
    }
}

3. 创建 JWT 认证

3.1 创建token包装类

3.2 创建JWT认证过滤器
/**
 * jwt认证的过滤器
 */
public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {

    private final AuthenticationFailureHandler failureHandler;

    public JwtTokenAuthenticationProcessingFilter(AuthenticationFailureHandler failureHandler, RequestMatcher matcher) {
        super(matcher);
        this.failureHandler = failureHandler;
    }

    /**
     * 身份认证
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        RawAccessJwtToken token = new RawAccessJwtToken(extract(request));
        return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token));
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authResult);
        SecurityContextHolder.setContext(context);
        chain.doFilter(request, response);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        failureHandler.onAuthenticationFailure(request, response, failed);
    }

    /**
     * 解析http请求中token
     */
    private String extract(HttpServletRequest request) {
        String header = request.getHeader(SotaSecurityConfiguration.JWT_TOKEN_HEADER_PARAM);
        if (StringUtils.isEmpty(header)) {
            throw new AuthenticationServiceException("Authorization header cannot be blank!");
        }
        if (header.length() < HEADER_PREFIX.length()) {
            throw new AuthenticationServiceException("Invalid authorization header size.");
        }
        return header.substring(HEADER_PREFIX.length(), header.length());
    }
}
3.3 创建 JWT 身份认证类
/**
 * jwt认证的具体实现
 */
@Component
@SuppressWarnings("unchecked")
public class JwtAuthenticationProvider implements AuthenticationProvider {

    private final JwtTokenFactory tokenFactory;

    @Autowired
    public JwtAuthenticationProvider(JwtTokenFactory tokenFactory) {
        this.tokenFactory = tokenFactory;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();
        SecurityUser securityUser = tokenFactory.parseAccessJwtToken(rawAccessToken);
        return new JwtAuthenticationToken(securityUser);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
    }
}

4. Spring Security 配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SotaSecurityConfiguration extends WebSecurityConfigurerAdapter {

    public static final String JWT_TOKEN_QUERY_PARAM = "token";
    public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
    public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/rest/**";
    public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
    public static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[]{"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"};

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private AuthenticationSuccessHandler successHandler;

    @Autowired
    private AuthenticationFailureHandler failureHandler;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    @Qualifier("jwtHeaderTokenExtractor")
    private TokenExtractor jwtHeaderTokenExtractor;

    @Autowired
    private RestAuthenticationProvider restAuthenticationProvider;

    @Autowired
    private JwtAuthenticationProvider jwtAuthenticationProvider;

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

    /**
     * 注册Authentication Provider,使用自定义身份验证组件
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(restAuthenticationProvider);
        auth.authenticationProvider(jwtAuthenticationProvider);
    }

    /**
     * 设置 HTTP 验证规则
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers()
                // 禁用缓存
                .cacheControl().disable().frameOptions().disable()
                .and()
                .cors()
                .and()
                // 禁用csrf
                .csrf().disable()
                .exceptionHandling()
                .and()
                // 使用token所以禁用session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 不需要认证接口
                .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll()
                // 需要认证接口
                .antMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated()
                .and()
                // 登录过滤器
                .addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
                // JWT认证过滤器
                .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    /**
     * 身份验证过滤器注册
     */
    @Bean
    protected RestLoginProcessingFilter buildRestLoginProcessingFilter() throws Exception {
        RestLoginProcessingFilter filter = new RestLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper);
        filter.setAuthenticationManager(this.authenticationManager);
        return filter;
    }

    /**
     * jwt认证注册
     */
    @Bean
    protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
        List<String> pathsToSkip = new ArrayList(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS));
        pathsToSkip.addAll(Arrays.asList(FORM_BASED_LOGIN_ENTRY_POINT));
        SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
        JwtTokenAuthenticationProcessingFilter filter = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher);
        filter.setAuthenticationManager(this.authenticationManager);
        return filter;
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

5. JWT token 工具类

实际生产环境中,不建议使用敏感信息加密生成token

5.1 JWT 配置类
@Configuration
@ConfigurationProperties(prefix = "security.jwt")
public class SotaJwtSettings {

     // 将在此之后过期
    private Integer tokenExpirationTime;
     // 令牌发行人 
    private String tokenIssuer;
     // 密钥用于签名
    private String tokenSigningKey;
     // 可以在这段时间内刷新 
    private Integer refreshTokenExpTime;

    public Integer getRefreshTokenExpTime() {
        return refreshTokenExpTime;
    }

    public void setRefreshTokenExpTime(Integer refreshTokenExpTime) {
        this.refreshTokenExpTime = refreshTokenExpTime;
    }

    public Integer getTokenExpirationTime() {
        return tokenExpirationTime;
    }

    public void setTokenExpirationTime(Integer tokenExpirationTime) {
        this.tokenExpirationTime = tokenExpirationTime;
    }

    public String getTokenIssuer() {
        return tokenIssuer;
    }

    public void setTokenIssuer(String tokenIssuer) {
        this.tokenIssuer = tokenIssuer;
    }

    public String getTokenSigningKey() {
        return tokenSigningKey;
    }
    
    public void setTokenSigningKey(String tokenSigningKey) {
        this.tokenSigningKey = tokenSigningKey;
    }
5.2 token 工具类
@Component
public class JwtTokenFactory {

    private static final String SCOPES = "scopes";
    private static final String USER_ID = "id";
    private static final String USER_NAME = "username";
    private static final String PASSWORD = "password";

    private final SotaJwtSettings settings;

    @Autowired
    public JwtTokenFactory(SotaJwtSettings settings) {
        this.settings = settings;
    }

    /**
     * 创建token
     */
    public String createAccessJwtToken(SecurityUser securityUser) {
        if (StringUtils.isEmpty(securityUser.getUsername())) {
            throw new IllegalArgumentException("Cannot create JWT Token without username/email");
        }
        if (securityUser.getAuthority() == null) {
            throw new IllegalArgumentException("User doesn't have any privileges");
        }
        Claims claims = Jwts.claims().setSubject(securityUser.getEmail());
        claims.put(SCOPES, securityUser.getAuthorities().stream().map(s -> s.getAuthority()).collect(Collectors.toList()));
        claims.put(USER_ID, securityUser.getId());
        claims.put(USER_NAME, securityUser.getUsername());
        claims.put(PASSWORD, securityUser.getPassword());

        Instant instant = Instant.now();
        // 生成token
        String token = Jwts.builder()
                .setClaims(claims)
                .setIssuer(settings.getTokenIssuer())
                .setIssuedAt(Date.from(instant))
                .setExpiration(Date.from(instant.plusSeconds(settings.getTokenExpirationTime())))
                .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey())
                .compact();
        return token;
    }

    /**
     * 解析token获取用户信息
     */
    public SecurityUser parseAccessJwtToken(RawAccessJwtToken rawAccessToken) {
        Jws<Claims> jwsClaims = rawAccessToken.parseClaims(settings.getTokenSigningKey());
        Claims claims = jwsClaims.getBody();
        String subject = claims.getSubject();
        List<String> scopes = claims.get(SCOPES, List.class);
        if (scopes == null || scopes.isEmpty()) {
            throw new IllegalArgumentException("JWT Token doesn't have any scopes");
        }
        SecurityUser securityUser = new SecurityUser();
        securityUser.setId(claims.get(USER_ID, Integer.class));
        securityUser.setUsername(claims.get(USER_NAME, String.class));
        securityUser.setPassword(claims.get(PASSWORD, String.class));
        securityUser.setEmail(subject);
        securityUser.setAuthority(scopes.get(0));
        return securityUser;
    }
}
Spring Boot 是一个用于构建微服务的开源框架,它能够快速搭建项目并且提供了许多便捷的功能和特性。Spring Security 是一个用于处理认证和授权的框架,可以保护我们的应用程序免受恶意攻击。JWT(JSON Web Token)是一种用于身份验证的开放标准,可以被用于安全地传输信息。Spring MVC 是一个用于构建 Web 应用程序的框架,它能够处理 HTTP 请求和响应。MyBatis 是一个用于操作数据库的框架,可以简化数据库操作和提高效率。Redis 是一种高性能的键值存储系统,可以用于缓存与数据存储。 基于这些技术,可以搭建一个商城项目。Spring Boot 可以用于构建商城项目的后端服务,Spring Security 可以确保用户信息的安全性,JWT 可以用于用户的身份验证,Spring MVC 可以处理前端请求,MyBatis 可以操作数据库,Redis 可以用于缓存用户信息和商品信息。 商城项目的后端可以使用 Spring BootSpring Security 来搭建,通过 JWT 来处理用户的身份验证和授权。数据库操作可以使用 MyBatis 来简化与提高效率,同时可以利用 Redis 来缓存一些常用的数据和信息,提升系统的性能。前端请求则可以通过 Spring MVC 来处理,实现商城项目的整体功能。 综上所述,借助于 Spring BootSpring SecurityJWTSpring MVC、MyBatis 和 Redis 这些技术,可以构建出一个高性能、安全可靠的商城项目,为用户提供良好的购物体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值