Spring Boot + Spring Security + JWT + MySQL构建安全认证
2020/3/15 17:01:46
本文主要是介绍Spring Boot + Spring Security + JWT + MySQL构建安全认证,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
转自连接: www.callicoder.com/spring-boot…
原文作者: Rajeev Kumar Singh
原文作者更多文章: www.callicoder.com/
github: github.com/callicoder/…
1. 本次构建安全架构概述
- 实现通过填写name, username, email 和 password的注册api
- 实现通过填写用户名(或邮箱)和密码的登录api.
- 用户信息验证成功后, 服务方生成 JWT 认证 token, 并返回给客户端.
- 客户端通过在Authorization 请求头中传递JWT token来访问特定资源.
- 通过配置 Spring security 来限制特定资源的访问权限, 例如:
- 登录, 注册 及 所有静态资源是完全开放的
- 其他特定资源只对已认证用户开放
- 配置 Spring security 以实现在无有效JWT token验证情况下访问特定资源时抛出401未授权错误
- 配置基于角色的授权机制来保护服务器资源.
2. 配置 Spring Security 和 JWT
代码
SecurityConfig
package com.example.polls.config; import com.example.polls.security.CustomUserDetailsService; import com.example.polls.security.JwtAuthenticationEntryPoint; import com.example.polls.security.JwtAuthenticationFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.BeanIds; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 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.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity( securedEnabled = true, jsr250Enabled = true, prePostEnabled = true ) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired CustomUserDetailsService customUserDetailsService; @Autowired private JwtAuthenticationEntryPoint unauthorizedHandler; @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } @Override public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder .userDetailsService(customUserDetailsService) .passwordEncoder(passwordEncoder()); } @Bean(BeanIds.AUTHENTICATION_MANAGER) @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http .cors() .and() .csrf() .disable() .exceptionHandling() .authenticationEntryPoint(unauthorizedHandler) .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/", "/favicon.ico", "/**/*.png", "/**/*.gif", "/**/*.svg", "/**/*.jpg", "/**/*.html", "/**/*.css", "/**/*.js") .permitAll() .antMatchers("/api/auth/**") .permitAll() .antMatchers("/api/user/checkUsernameAvailability", "/api/user/checkEmailAvailability") .permitAll() .antMatchers(HttpMethod.GET, "/api/polls/**", "/api/users/**") .permitAll() .anyRequest() .authenticated(); // Add our custom JWT security filter http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } } 复制代码
代码解读
1. @EnableWebSecurity
- 开启 web security
2. @EnableGlobalMethodSecurity
- 开启基于断言的方法级保护, 3种类型:
- securedEnabled, 开启 @Secured 断言, 保护controller/service 的方法:
@Secured("ROLE_ADMIN") public User getAllUsers() {} @Secured({"ROLE_USER", "ROLE_ADMIN"}) public User getUser(Long id) {} @Secured("IS_AUTHENTICATED_ANONYMOUSLY") public boolean isUsernameAvailable() {} 复制代码
- jsr250Enabled, 开启 @RolesAllowed 断言:
@RolesAllowed("ROLE_ADMIN") public Poll createPoll() {} 复制代码
- prePostEnabled, 开启通过@PreAuthorize 和 @PostAuthorize 控制的更复杂的权限控制表达式:
@PreAuthorize("isAnonymous()") public boolean isUsernameAvailable() {} @PreAuthorize("hasRole('USER')") public Poll createPoll() {} 复制代码
3. WebSecurityConfigurerAdapter
- 提供默认的安全配置, 允许其他类继承并通过覆盖方法的方式自定义安全配置.
- SecurityConfig 继承于此类, 并覆盖某些方法来自定义安全配置.
4. CustomUserDetailsService
- 实现接口UserDetailsService.
- 通过覆盖接口的loadUserByUsername方法来实现认证用户操作.
5. JwtAuthenticationEntryPoint
- 实现 AuthenticationEntryPoint 接口.
- 对于没有通过认证而访问特殊资源的用户返回401未授权错误.
6. JwtAuthenticationFilter
- 从所有请求的Authorization请求头中获取 JWT token
- 验证token
- 加载token相关的用户信息
- 在Spring Security 的安全容器中设置用户信息. Spring Security 使用此用户信息操作授权验证. 我们可以在controller中通过获取安全容器中的用户信息来操作我们的业务逻辑.
7. AuthenticationManagerBuilder 和 AuthenticationManager
- AuthenticationManager 是 Spring Security 中实现用户认证的主要接口
- AuthenticationManagerBuilder 负责生成 AuthenticationManager
- 可以通过AuthenticationManagerBuilder 建立基于内存的认证, LDAP 认证, JDBC认证, 或者添加自己的自定义认证.
- 此例子中, 我们提供 customUserDetailsService 和 passwordEncoder来构建AuthenticationManager.
- 通过配置好的AuthenticationManager在登录api中认证用户.
8. HttpSecurity configurations
- HttpSecurity 配置信息包括csrf, session管理, 基于不同条件的资源保护策略等安全配置功能.
- 此例子中, 我们开发静态资源及一些公共api, 其他资源则限制只有认证用户可访问.
- 我们在配置中加入了 JWTAuthenticationEntryPoint 和 自定义的 JWTAuthenticationFilter.
3. 创建自定义Spring Security Classes, Filters, and Annotations
3.1. 自定义 Spring Security AuthenticationEntryPoint
- 定义 JwtAuthenticationEntryPoint 实现 AuthenticationEntryPoint 接口及其中的commence方法.
- commence方法会在未认证用户视图访问需要认证的资源时触发.
- 此例子中, 我们简单的返回包含异常信息的 401 错误.
JwtAuthenticationEntryPoint
package com.example.polls.security; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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; @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class); @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { logger.error("Responding with unauthorized error. Message - {}", e.getMessage()); httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); } } 复制代码
3.2. 自定义 Spring Security UserDetails
- UserPrincipal 实现 UserDetails接口.
- 自定义UserDetailsService接口会返回此对象.
- Spring Security使用此信息进行认证及授权操作.
UserPrincipal
package com.example.polls.security; import com.example.polls.model.User; import com.fasterxml.jackson.annotation.JsonIgnore; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; public class UserPrincipal implements UserDetails { private Long id; private String name; private String username; @JsonIgnore private String email; @JsonIgnore private String password; private Collection<? extends GrantedAuthority> authorities; public UserPrincipal(Long id, String name, String username, String email, String password, Collection<? extends GrantedAuthority> authorities) { this.id = id; this.name = name; this.username = username; this.email = email; this.password = password; this.authorities = authorities; } public static UserPrincipal create(User user) { List<GrantedAuthority> authorities = user.getRoles().stream().map(role -> new SimpleGrantedAuthority(role.getName().name()) ).collect(Collectors.toList()); return new UserPrincipal( user.getId(), user.getName(), user.getUsername(), user.getEmail(), user.getPassword(), authorities ); } public Long getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserPrincipal that = (UserPrincipal) o; return Objects.equals(id, that.id); } @Override public int hashCode() { return Objects.hash(id); } } 复制代码
3.3. 自定义 Spring Security UserDetailsService
- 通过用户名加载用户数据
CustomUserDetailsService
package com.example.polls.security; import com.example.polls.exception.ResourceNotFoundException; import com.example.polls.model.User; import com.example.polls.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class CustomUserDetailsService implements UserDetailsService { @Autowired UserRepository userRepository; @Override @Transactional public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException { User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail) .orElseThrow(() -> new UsernameNotFoundException("User not found with username or email : " + usernameOrEmail) ); return UserPrincipal.create(user); } @Transactional public UserDetails loadUserById(Long id) { User user = userRepository.findById(id).orElseThrow( () -> new ResourceNotFoundException("User", "id", id) ); return UserPrincipal.create(user); } } 复制代码
3.4. 生成及验证JWT 的 工具类
- JwtTokenProvider 在用户登录成功后生成JWT, 并负责验证Authorization请求头中的JWT.
- 在application.properties中定义 JWT 密钥及过期时间
## jwt Properties app.jwtSecret= JWTSuperSecretKey app.jwtExpirationInMs = 604800000 复制代码
JwtTokenProvider
package com.example.polls.security; import io.jsonwebtoken.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import java.util.Date; @Component public class JwtTokenProvider { private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class); @Value("${app.jwtSecret}") private String jwtSecret; @Value("${app.jwtExpirationInMs}") private int jwtExpirationInMs; public String generateToken(Authentication authentication) { UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); Date now = new Date(); Date expiryDate = new Date(now.getTime() + jwtExpirationInMs); return Jwts.builder() .setSubject(Long.toString(userPrincipal.getId())) .setIssuedAt(new Date()) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, jwtSecret) .compact(); } public Long getUserIdFromJWT(String token) { Claims claims = Jwts.parser() .setSigningKey(jwtSecret) .parseClaimsJws(token) .getBody(); return Long.parseLong(claims.getSubject()); } public boolean validateToken(String authToken) { try { Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken); return true; } catch (SignatureException ex) { logger.error("Invalid JWT signature"); } catch (MalformedJwtException ex) { logger.error("Invalid JWT token"); } catch (ExpiredJwtException ex) { logger.error("Expired JWT token"); } catch (UnsupportedJwtException ex) { logger.error("Unsupported JWT token"); } catch (IllegalArgumentException ex) { logger.error("JWT claims string is empty."); } return false; } } 复制代码
3.5. 自定义 Spring Security AuthenticationFilter
- JWTAuthenticationFilter 从请求中获取JWT token, 验证, 加载token相关用户信息, 并传递给 Spring Security.
JwtAuthenticationFilter
package com.example.polls.security; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 JwtTokenProvider tokenProvider; @Autowired private CustomUserDetailsService customUserDetailsService; private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = getJwtFromRequest(request); if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { Long userId = tokenProvider.getUserIdFromJWT(jwt); // 可以将用户名及角色信息都编码到 JWT claims中 // 然后通过解析JWT 的 claims 对象来 创建UserDetails信息 // 避免重复查询数据库 UserDetails userDetails = customUserDetailsService.loadUserById(userId); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception ex) { logger.error("Could not set user authentication in security context", ex); } filterChain.doFilter(request, response); } 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; } } 复制代码
3.6. 自定义断言获取当前用户
- Spring Security 提供 @AuthenticationPrincipal 断言在Controller 中获取当前认证的用户
- CurrentUser 断言封装了 AuthenticationPrincipal
CurrentUser
package com.example.polls.security; import org.springframework.security.core.annotation.AuthenticationPrincipal; import java.lang.annotation.*; @Target({ElementType.PARAMETER, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @AuthenticationPrincipal public @interface CurrentUser { } 复制代码
- 通过创建元断言, 可以避免在工程中过多绑定Spring Security的断言,减少对Spring Security 的依赖. 当需要移除Spring Security的时候, 可以简单的修改 CurrentUser 断言.
4. 登录注册API
4.1. 请求Payloads
LoginRequest
import javax.validation.constraints.NotBlank; import lombok.Data; @Data public class LoginRequest { @NotBlank private String usernameOrEmail; @NotBlank private String password; } 复制代码
SignUpRequest
import javax.validation.constraints.*; import lombok.Data; @Data public class SignUpRequest { @NotBlank @Size(min = 4, max = 40) private String name; @NotBlank @Size(min = 3, max = 15) private String username; @NotBlank @Size(max = 40) @Email private String email; @NotBlank @Size(min = 6, max = 20) private String password; // TODO: 手机号 } 复制代码
4.2. 响应Payloads
JwtAuthenticationResponse
import lombok.Data; @Data public class JwtAuthenticationResponse { private String accessToken; private String tokenType = "Bearer"; public JwtAuthenticationResponse(String accessToken) { this.accessToken = accessToken; } } 复制代码
ApiResponse
import lombok.Data; @Data public class ApiResponse { private Boolean success; private String message; public ApiResponse(Boolean success, String message) { this.success = success; this.message = message; } } 复制代码
4.3. 自定义业务异常
AppException
import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public class AppException extends RuntimeException { public AppException(String message) { super(message); } public AppException(String message, Throwable cause) { super(message, cause); } } 复制代码
BadRequestException
package com.example.polls.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(HttpStatus.BAD_REQUEST) public class BadRequestException extends RuntimeException { public BadRequestException(String message) { super(message); } public BadRequestException(String message, Throwable cause) { super(message, cause); } } 复制代码
ResourceNotFoundException
package com.example.polls.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(HttpStatus.NOT_FOUND) public class ResourceNotFoundException extends RuntimeException { private String resourceName; private String fieldName; private Object fieldValue; public ResourceNotFoundException( String resourceName, String fieldName, Object fieldValue) { super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue)); this.resourceName = resourceName; this.fieldName = fieldName; this.fieldValue = fieldValue; } public String getResourceName() { return resourceName; } public String getFieldName() { return fieldName; } public Object getFieldValue() { return fieldValue; } } 复制代码
4.4. 认证 Controller
AuthController
package com.example.polls.controller; import com.example.polls.exception.AppException; import com.example.polls.model.Role; import com.example.polls.model.RoleName; import com.example.polls.model.User; import com.example.polls.payload.ApiResponse; import com.example.polls.payload.JwtAuthenticationResponse; import com.example.polls.payload.LoginRequest; import com.example.polls.payload.SignUpRequest; import com.example.polls.repository.RoleRepository; import com.example.polls.repository.UserRepository; import com.example.polls.security.JwtTokenProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import javax.validation.Valid; import java.net.URI; import java.util.Collections; @RestController @RequestMapping("/api/auth") public class AuthController { @Autowired AuthenticationManager authenticationManager; @Autowired UserRepository userRepository; @Autowired RoleRepository roleRepository; @Autowired PasswordEncoder passwordEncoder; @Autowired JwtTokenProvider tokenProvider; @PostMapping("/signin") public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsernameOrEmail(), loginRequest.getPassword() ) ); SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = tokenProvider.generateToken(authentication); return ResponseEntity.ok(new JwtAuthenticationResponse(jwt)); } @PostMapping("/signup") public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) { if(userRepository.existsByUsername(signUpRequest.getUsername())) { return new ResponseEntity(new ApiResponse(false, "Username is already taken!"), HttpStatus.BAD_REQUEST); } if(userRepository.existsByEmail(signUpRequest.getEmail())) { return new ResponseEntity(new ApiResponse(false, "Email Address already in use!"), HttpStatus.BAD_REQUEST); } // Creating user's account User user = new User(signUpRequest.getName(), signUpRequest.getUsername(), signUpRequest.getEmail(), signUpRequest.getPassword()); user.setPassword(passwordEncoder.encode(user.getPassword())); Role userRole = roleRepository.findByName(RoleName.ROLE_USER) .orElseThrow(() -> new AppException("User Role not set.")); user.setRoles(Collections.singleton(userRole)); User result = userRepository.save(user); URI location = ServletUriComponentsBuilder .fromCurrentContextPath().path("/users/{username}") .buildAndExpand(result.getUsername()).toUri(); return ResponseEntity.created(location).body(new ApiResponse(true, "User registered successfully")); } } 复制代码
这篇关于Spring Boot + Spring Security + JWT + MySQL构建安全认证的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-12-26大厂数据结构与算法教程:入门级详解
- 2024-12-26大厂算法与数据结构教程:新手入门指南
- 2024-12-26Python编程入门指南
- 2024-12-26数据结构高级教程:新手入门及初级提升指南
- 2024-12-26并查集入门教程:从零开始学会并查集
- 2024-12-26大厂数据结构与算法入门指南
- 2024-12-26大厂算法与数据结构入门教程
- 2024-12-26二叉树入门教程:轻松掌握基础概念与操作
- 2024-12-26初学者指南:轻松掌握链表
- 2024-12-26平衡树入门教程:轻松理解与应用