- 实现通过填写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 的 工具类
## 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 Security官方文档
原文
https://juejin.im/post/5e6cf3ab51882549112b544c
本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » Spring Boot + Spring Security + JWT + MySQL构建安全认证