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); } } 复制代码
@Secured("ROLE_ADMIN") public User getAllUsers() {} @Secured({"ROLE_USER", "ROLE_ADMIN"}) public User getUser(Long id) {} @Secured("IS_AUTHENTICATED_ANONYMOUSLY") public boolean isUsernameAvailable() {} 复制代码
@RolesAllowed("ROLE_ADMIN") public Poll createPoll() {} 复制代码
@PreAuthorize("isAnonymous()") public boolean isUsernameAvailable() {} @PreAuthorize("hasRole('USER')") public Poll createPoll() {} 复制代码
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()); } } 复制代码
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); } } 复制代码
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); } } 复制代码
## jwt Properties app.jwtSecret= JWTSuperSecretKey app.jwtExpirationInMs = 604800000 复制代码
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; } } 复制代码
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; } } 复制代码
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 { } 复制代码
import javax.validation.constraints.NotBlank; import lombok.Data; @Data public class LoginRequest { @NotBlank private String usernameOrEmail; @NotBlank private String password; } 复制代码
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: 手机号 } 复制代码
import lombok.Data; @Data public class JwtAuthenticationResponse { private String accessToken; private String tokenType = "Bearer"; public JwtAuthenticationResponse(String accessToken) { this.accessToken = accessToken; } } 复制代码
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; } } 复制代码
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); } } 复制代码
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); } } 复制代码
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; } } 复制代码
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官方文档