Spring Security
简介
Spring Security 是一款安全框架。
基本使用
引入依赖包:
Developer Tools
Web
Template Engines
Security
Spring Security
OAuth2 Resource Server
SQL
Spring Data JPA / MyBatis Framework
MySQL Driver / …
书写配置文件:
1 2 3 4 5 6 spring.datasource.url=${MYSQL_URI:jdbc:mysql://xxx.xxx.xxx:xxxx/xxx} spring.datasource.username=${JDBC_USERNAME:xxx} spring.datasource.password=${JDBC_PASSWORD:xxx} spring.datasource.driver-class-name=${JDBC_DRIVER:com.mysql.cj.jdbc.Driver} spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl spring.jpa.hibernate.ddl-auto=update
新建用户模型 user/User.java
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import com.fasterxml.jackson.annotation.JsonIgnore;import jakarta.persistence.*;import lombok.*;import org.hibernate.proxy.HibernateProxy;import java.util.Objects;@Getter @Setter @ToString @RequiredArgsConstructor @Entity @Table(name="T_USER") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; private String roles; public User (String username, String password, String roles) { this .username = username; this .password = password; this .roles = roles; } @Override public final boolean equals (Object o) { if (this == o) return true ; if (o == null ) return false ; Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this ).getHibernateLazyInitializer().getPersistentClass() : this .getClass(); if (thisEffectiveClass != oEffectiveClass) return false ; User user = (User) o; return getId() != null && Objects.equals(getId(), user.getId()); } @Override public final int hashCode () { return this instanceof HibernateProxy ? ((HibernateProxy) this ).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); } }
新建用户存储库 user/UserRepository.java
:
1 2 3 4 5 6 7 8 9 10 11 12 import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.data.jpa.repository.JpaSpecificationExecutor;import org.springframework.stereotype.Repository;import java.util.Optional;@Repository public interface UserRepository extends JpaRepository <User, Long>, JpaSpecificationExecutor<User> { Optional<User> findByUsername (String username) ; }
新建用户初始化类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import org.springframework.context.ApplicationListener;import org.springframework.context.event.ContextRefreshedEvent;import org.springframework.lang.Nullable;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.stereotype.Component;import org.springframework.transaction.annotation.Transactional;import java.util.Optional;@Component public class SetupUserLoader implements ApplicationListener <ContextRefreshedEvent> { boolean alreadySetup = false ; private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public SetupUserLoader (UserRepository userRepository, PasswordEncoder passwordEncoder) { this .userRepository = userRepository; this .passwordEncoder = passwordEncoder; } @Override @Transactional public void onApplicationEvent (@Nullable ContextRefreshedEvent event) { if (alreadySetup) return ; createUserIfNotFound("admin" , "admin" , "ROLE_ADMIN,ROLE_USER" ); alreadySetup = true ; } @Transactional public void createUserIfNotFound (String username, String password, String role) { Optional<User> optional = userRepository.findByUsername(username); if (optional.isEmpty()) { User user = new User (); user.setUsername(username); user.setPassword(passwordEncoder.encode(password)); user.setRoles(role); userRepository.save(user); } } }
新建登录请求类 auth/LoginRequest.java
:
1 2 3 4 5 6 7 8 9 10 import lombok.Data;@Data public class LoginRequest { public String username; public String password; }
新建用户细节类 security/CustomUserDetail.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import java.util.Arrays;import java.util.Collection;public class CustomUserDetail implements UserDetails { private final User user; public CustomUserDetail (User user) { this .user = user; } @Override public Collection<? extends GrantedAuthority > getAuthorities() { return Arrays.stream(user .getRoles() .split("," )) .map(SimpleGrantedAuthority::new ) .toList(); } @Override public String getPassword () { return user.getPassword(); } @Override public String getUsername () { return user.getUsername(); } @Override public boolean isAccountNonExpired () { return true ; } @Override public boolean isAccountNonLocked () { return true ; } @Override public boolean isCredentialsNonExpired () { return true ; } @Override public boolean isEnabled () { return true ; } }
新建登录验证程序 security/CustomUserDetailsService
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import jakarta.annotation.Resource;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 java.util.Optional;@Service public class CustomUserDetailsService implements UserDetailsService { @Resource private UserRepository userRepository; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { Optional<User> optional = userRepository.findByUsername(username); if (optional.isEmpty()) { throw new UsernameNotFoundException (username); } return new CustomUserDetail (optional.get()); } }
Session
新建登出处理类 security/NoRedirectLogoutSuccessHandler.java
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import jakarta.servlet.ServletException;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import org.springframework.security.core.Authentication;import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;import java.io.IOException;public class NoRedirectLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess (HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setStatus(200 ); } }
新建权限配置程序 security/SecurityConfig.java
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 import jakarta.annotation.Resource;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.ProviderManager;import org.springframework.security.authentication.dao.DaoAuthenticationProvider;import org.springframework.security.config.Customizer;import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;import org.springframework.security.config.annotation.rsocket.EnableRSocketSecurity;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.configurers.AbstractHttpConfigurer;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.logout.HeaderWriterLogoutHandler;import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter;import static jakarta.servlet.DispatcherType.ERROR;import static jakarta.servlet.DispatcherType.FORWARD;import static org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive.ALL;@Configuration @EnableWebSecurity @EnableMethodSecurity @EnableRSocketSecurity public class SecurityConfig { @Resource private CustomUserDetailsService customUserDetailsService; private static final String ADMIN_ROLE_NAME = "ROLE_ADMIN" ; private static final String[] AUTH_WHITELIST = { "/v3/api-docs/**" , "/swagger-ui/**" , "/api/auth/login" }; @Bean public SecurityFilterChain securityFilterChain (HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize .dispatcherTypeMatchers(FORWARD, ERROR).permitAll() .requestMatchers(AUTH_WHITELIST).permitAll() .requestMatchers("/api/admin/**" ).hasAuthority(ADMIN_ROLE_NAME) .anyRequest().authenticated() ) .csrf(AbstractHttpConfigurer::disable) .httpBasic(Customizer.withDefaults()) .cors(AbstractHttpConfigurer::disable) .formLogin(Customizer.withDefaults()) .logout((logout) -> logout .logoutUrl("/api/auth/logout" ) .addLogoutHandler(new HeaderWriterLogoutHandler (new ClearSiteDataHeaderWriter (ALL))) .deleteCookies("JSESSIONID" ) .logoutSuccessHandler(new NoRedirectLogoutSuccessHandler ()) ); return http.build(); } @Bean public PasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder (); } @Bean public AuthenticationManager authenticationManager () { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider (); daoAuthenticationProvider.setUserDetailsService(customUserDetailsService); daoAuthenticationProvider.setPasswordEncoder(passwordEncoder()); return new ProviderManager (daoAuthenticationProvider); } }
新建用户验证服务 auth/AuthService.java
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import jakarta.annotation.Resource;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.Authentication;import org.springframework.security.core.AuthenticationException;import org.springframework.security.core.context.SecurityContext;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.security.core.context.SecurityContextHolderStrategy;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;import org.springframework.security.web.context.HttpSessionSecurityContextRepository;import org.springframework.security.web.context.SecurityContextRepository;import org.springframework.stereotype.Service;@Service public class AuthService { @Resource private AuthenticationManager authenticationManager; private final SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository (); SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler (); public void login (HttpServletRequest request, HttpServletResponse response, LoginRequest body ) throws AuthenticationException { UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(body.getUsername(), body.getPassword()); Authentication authentication = authenticationManager.authenticate(token); SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy(); SecurityContext context = securityContextHolderStrategy.createEmptyContext(); context.setAuthentication(authentication); securityContextHolderStrategy.setContext(context); securityContextRepository.saveContext(context, request, response); } public String getUsername () { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof User user) { return user.getUsername(); } else if (principal instanceof UserDetails userDetails) { return userDetails.getUsername(); } else { throw new ReportBadException (ErrorEnum.PARAM_EXCEPTION); } } }
新建登录控制器 auth/AuthController.java
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import jakarta.annotation.Resource;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import org.springframework.web.bind.annotation.*;@RestController @RequestMapping("/api/auth") public class AuthController { @Resource private AuthService authService; @PostMapping("/login") public void login (@RequestBody LoginRequest loginRequest, HttpServletRequest request, HttpServletResponse response) { authService.login(request,response,loginRequest); } @GetMapping("/user") public String getUser () { return authService.getUsername(); } }
使用方式
在 IDEA 中则可以使用如下方式进行请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ### Login POST http://localhost:8080/api/auth/login Content-Type: application/json { "username": "admin", "password": "admin" } ### Get User GET http://localhost:8080/api/auth/user Content-Type: application/json ### Logout GET http://localhost:8080/api/auth/logout Content-Type: application/json
获取用户相关信息的基本方式
1 2 3 4 5 6 7 8 @RestController public class HelloController { @GetMapping("/hello") public String hello () { return "当前登录用户:" + SecurityContextHolder.getContext().getAuthentication().getName(); } }
JWT
新增如下配置:
1 2 jwt.public.key=classpath:pub.key jwt.private.key=classpath:pri.key
新建权限配置程序 security/SecurityConfig.java
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 import com.nimbusds.jose.jwk.JWK;import com.nimbusds.jose.jwk.JWKSet;import com.nimbusds.jose.jwk.RSAKey;import com.nimbusds.jose.jwk.source.ImmutableJWKSet;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.Customizer;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.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;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.data.repository.query.SecurityEvaluationContextExtension;import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;import org.springframework.security.oauth2.jwt.JwtDecoder;import org.springframework.security.oauth2.jwt.JwtEncoder;import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;import org.springframework.security.web.SecurityFilterChain;import java.security.interfaces.RSAPrivateKey;import java.security.interfaces.RSAPublicKey;@Configuration @EnableWebSecurity public class SecurityConfig { @Value("${jwt.public.key}") RSAPublicKey publicKey; @Value("${jwt.private.key}") RSAPrivateKey privateKey; private static final String ADMIN_ROLE_NAME = "ROLE_ADMIN" ; private static final String USER_ROLE_NAME = "ROLE_USER" ; private static final String[] AUTH_WHITELIST = { "/v3/api-docs/**" , "/swagger-ui/**" , "/api/v1/user/login" }; @Bean public SecurityFilterChain securityFilterChain (HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize .dispatcherTypeMatchers(FORWARD, ERROR).permitAll() .requestMatchers(AUTH_WHITELIST).permitAll() .requestMatchers(AUTH_WHITELIST).permitAll() .requestMatchers("/api/v1/admin/**" ).hasAuthority(ADMIN_ROLE_NAME) .requestMatchers("/api/v1/admin" ).hasAuthority(ADMIN_ROLE_NAME) .requestMatchers("/api/v1/notice" ).hasAnyAuthority(USER_ROLE_NAME, ADMIN_ROLE_NAME) .anyRequest().authenticated() ) .csrf((csrf) -> csrf.ignoringRequestMatchers("/token" )) .httpBasic(Customizer.withDefaults()) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .decoder(jwtDecoder()) ) ) .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .exceptionHandling((exceptions) -> exceptions.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint ()) .accessDeniedHandler(new BearerTokenAccessDeniedHandler ()) ).cors(AbstractHttpConfigurer::disable); return http.build(); } @Bean public PasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder (); } @Bean JwtDecoder jwtDecoder () { return NimbusJwtDecoder.withPublicKey(this .publicKey).build(); } @Bean JwtEncoder jwtEncoder () { JWK jwk = new RSAKey .Builder(this .publicKey).privateKey(this .privateKey).build(); return new NimbusJwtEncoder (new ImmutableJWKSet <>(new JWKSet (jwk))); } }
新建登录返回类 auth/LoginResponse.java
:
1 2 3 4 5 6 import lombok.Data;@Data public class LoginResponse { private String token; }
新建用户验证服务 auth/AuthService.java
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import jakarta.annotation.Resource;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Value;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.oauth2.jwt.JwtClaimsSet;import org.springframework.security.oauth2.jwt.JwtEncoder;import org.springframework.security.oauth2.jwt.JwtEncoderParameters;import org.springframework.stereotype.Service;import java.time.Instant;import java.util.Optional;import java.util.stream.Collectors;@Slf4j @Service public class UserService { @Value("${jwt.expiry:36000}") Long expiry; @Resource private JwtEncoder jwtEncoder; @Resource private UserDetailsService userDetailsService; @Resource private PasswordEncoder passwordEncoder; @Resource private UserRepository userRepository; public LoginResponse login (LoginRequest loginRequest) { LoginResponse loginResponse = new LoginResponse (); UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername()); if (passwordEncoder.matches(loginRequest.getPassword(), userDetails.getPassword())) { loginResponse.setToken(generateToken(userDetails)); } else { throw new RuntimeException ("Invalid password" ); } return loginResponse; } public User getUserByUsername (String username) { Optional<User> optionalUser = userRepository.findByUsername(username); if (optionalUser.isEmpty()){ throw new RuntimeException ("Not Found" ); } return optionalUser.get(); } private String generateToken (UserDetails userDetails) { Instant now = Instant.now(); String scope = userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(" " )); JwtClaimsSet claims = JwtClaimsSet.builder() .issuer("self" ) .issuedAt(now) .expiresAt(now.plusSeconds(expiry)) .subject(userDetails.getUsername()) .claim("scope" , scope) .build(); return this .jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue(); } }
新建登录控制器 auth/AuthController.java
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import io.swagger.v3.oas.annotations.Operation;import jakarta.annotation.Resource;import lombok.extern.slf4j.Slf4j;import org.springframework.http.ResponseEntity;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.security.oauth2.jwt.Jwt;import org.springframework.web.bind.annotation.*;@Slf4j @RestController @RequestMapping("/api/auth") public class AuthController { @Resource private UserService userService; @GetMapping @Operation(summary = "get current user") public ResponseEntity<User> user () { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof Jwt) { String username = ((Jwt) principal).getSubject(); return ResponseEntity.ok(userService.getUserByUsername(username)); } else { throw new RuntimeException ("Token error" ); } } @PostMapping("/login") @Operation(summary = "login") public ResponseEntity<LoginResponse> login (@RequestBody LoginRequest loginRequest) { return ResponseEntity.ok(userService.login(loginRequest)); } @GetMapping("/roles") public ResponseEntity<List<String>> authorities () { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); List<String> authorities = new ArrayList <>(); for (GrantedAuthority authority : authentication.getAuthorities()) { authorities.add(authority.getAuthority()); } return ResponseEntity.ok(authorities); } }
使用方式
1 2 3 4 5 6 curl --location --request POST 'localhost:8080/api/auth/login' \ --header 'Content-Type: application/json' \ --data-raw '{ "username":"admin", "password":"admin" }'
1 curl --request GET 'http://localhost:8080/api/auth' --header 'Authorization: Bearer <token>'
首先需要参照如下样例修改配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain (HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception { DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver (); resolver.setAllowUriQueryParameter(true ); resolver.setAllowFormEncodedBodyParameter(true ); http .authorizeHttpRequests((authorize) -> authorize .dispatcherTypeMatchers(FORWARD, ERROR).permitAll() .requestMatchers(antMatcher("/api/auth/login" )).permitAll() .anyRequest().authenticated() ) .csrf(AbstractHttpConfigurer::disable) .httpBasic(Customizer.withDefaults()) .oauth2ResourceServer(oauth2 -> oauth2 .bearerTokenResolver(resolver) .jwt(jwt -> jwt .decoder(jwtDecoder()) ) ) .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .exceptionHandling((exceptions) -> exceptions .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint ())); return http.build(); } }
然后即可在请求中添加 access_token
参数即可,GET 请求样例如下:
1 curl --request GET 'http://localhost:8080/api/user?access_token=<token>'
注:无需添加 Bearer
字段
OpenAPI 相关配置
可以加入如下配置,可以在 OpenAPI 中自动携带 JWT Token。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import io.swagger.v3.oas.models.Components;import io.swagger.v3.oas.models.OpenAPI;import io.swagger.v3.oas.models.security.SecurityRequirement;import io.swagger.v3.oas.models.security.SecurityScheme;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configuration public class OpenAPIConf { @Bean public OpenAPI customizeOpenAPI () { String securitySchemeName = "bearerAuth" ; return new OpenAPI () .addSecurityItem(new SecurityRequirement () .addList(securitySchemeName)) .components(new Components () .addSecuritySchemes(securitySchemeName, new SecurityScheme () .name(securitySchemeName) .type(SecurityScheme.Type.HTTP) .scheme("bearer" ) .description( "Provide the JWT token. JWT token can be obtained from the Login API. For testing, use the credentials <strong>john/password</strong>" ) .bearerFormat("JWT" ))); } }
与 Spring Data 集成
可以在依赖中添加 org.springframework.security:spring-security-data
来与 SpringData 集成,通过下面的查询直接返回用户所拥有的内容:
1 2 3 4 5 @Repository public interface MessageRepository extends PagingAndSortingRepository <Message,Long> { @Query("select m from Message m where m.to.id = ?#{ principal?.id }") Page<Message> findInbox (Pageable pageable) ; }
自定义登录页
编写如下 resources/templates/login.html
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <!DOCTYPE html > <html lang ="zh" xmlns:th ="https://www.thymeleaf.org" > <head > <title > Please Log In</title > </head > <body > <h1 > Here is Custom Login page. Please Log In: </h1 > <div th:if ="${param.error}" > Invalid username and password.</div > <div th:if ="${param.logout}" > You have been logged out.</div > <form th:action ="@{/login}" method ="post" > <div > <label > <input type ="text" name ="username" placeholder ="Username" /> </label > </div > <div > <label > <input type ="password" name ="password" placeholder ="Password" /> </label > </div > <input type ="submit" value ="Log in" /> </form > </body > </html >
编写 security/SecurityConfig.java
配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.web.SecurityFilterChain;@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain (HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) .formLogin(form -> form.loginPage("/login" ).permitAll()); return http.build(); } }
编写 auth/LoginController.java
:
1 2 3 4 5 6 7 8 9 10 11 12 import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;@Controller public class LoginController { @GetMapping("/login") public String login () { return "login" ; } }
注:此处如果单个页面样式上需要使用 Tailwind CSS 可以参照参考资料样例。
单元测试
在测试中可以使用如下方式指定测试用户:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class UserControllerTest { @Autowired private MockMvc mockMvc; @Test @WithMockUser(username = "user", roles = "USER") public void testGetUser () throws Exception { mockMvc.perform(get("/api/users/1" )) .andExpect(status().isOk()) .andExpect(jsonPath("$.id" ).value(1 )) .andExpect(jsonPath("$.username" ).value("user" )); } }
方法鉴权
除了在接口层面上做安全之外,还可以在方法层面上进行补充和完善,确保数据安全。
开启下面的注解后即可使用方法鉴权:
1 2 3 import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;@EnableMethodSecurity
在通过 Spring 调用相应需要鉴权的方法时就会触发安全检查,抛出相应异常。
参考资料
Spring Security 例程
baeldung 教程
RSA 密钥生成
spring-boot-3-jwt-security
spring-boot-tailwind
Spring Tips: Spring Security method security with special guest Rob Winch
method-security 样例项目
Let’s Explore Spring Security 6.4 (SpringOne 2024)
Bootiful Spring Boot 3.4: Spring Security
spring-security-64
Testing with CSRF Protection