实现前后端分离的一个重要问题是如何进行身份验证和授权。Spring Security提供了一个非常方便的方法来处理这个问题,即使用JSON Web Token(JWT)。
JWT是一种用于身份验证和授权的开放标准,它定义了一种紧凑的、自包含的、可自校验的JSON格式来传递信息,通常用于在安全领域的传输而被广泛使用。
下面是SpringSecurity+JWT实现前后端分离的详细攻略:
1. 添加依赖
首先需要添加以下依赖到项目中,可以使用Maven或者Gradle,这里以Maven为例:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2. 配置Spring Security
配置Spring Security的基本步骤如下:
-
创建一个实现了UserDetailsService接口的类,该类用于从数据库中获取用户信息;
-
创建一个继承了WebSecurityConfigurerAdapter类的security配置类,并覆盖一些默认配置;
-
配置PasswordEncoder用于对用户密码进行加密。
以下是一个简单的security配置类:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/authenticate").permitAll()
.anyRequest()
.authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()));
}
}
在上面的配置中,我们禁用了CSRF防护,对于POST请求中的/authenticate接口我们允许所有人访问,其他所有请求都需要验证用户身份。
3. 实现JWT
在Spring Security的配置类中,我们添加了JWTAuthenticationFilter和JWTAuthorizationFilter。JWTAuthenticationFilter用于在验证用户名和密码之后,生成一个JWT并将其添加到Http的Header中返回给客户端;JWTAuthorizationFilter用于在客户端发送请求时,对JWT进行验证并允许或拒绝访问。
这里我们需要实现两个过滤器:
JWTAuthenticationFilter
JWTAuthenticationFilter中的实现如下:
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
User creds = new ObjectMapper()
.readValue(req.getInputStream(), User.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException, ServletException {
String token = JWT.create()
.withSubject(((User) auth.getPrincipal()).getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.sign(HMAC512(SECRET.getBytes()));
res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
}
}
在JWTAuthenticationFilter中,我们覆盖了attemptAuthentication方法,当用户输入用户名和密码时,这个方法会被调用并使用authenticationManager对用户名和密码进行验证。
在验证成功时,我们使用JWT库生成一个JWT token并将其添加到Http的Header中返回给客户端。
JWTAuthorizationFilter
JWTAuthorizationFilter中的实现如下:
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
String header = req.getHeader(HEADER_STRING);
if (header == null || !header.startsWith(TOKEN_PREFIX)) {
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, res);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(HEADER_STRING);
if (token != null) {
// parse the token.
String user = JWT.require(HMAC512(SECRET.getBytes()))
.build()
.verify(token.replace(TOKEN_PREFIX, ""))
.getSubject();
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}
在JWTAuthorizationFilter中,我们覆盖了doFilterInternal方法,当客户端在Http的Header中发送了一个名为Authorization的JWT token时,这个方法会被调用。
在该方法中,我们获取JWT token并对其进行验证,如果验证成功,将该用户的信息添加到SecurityContextHolder中,允许用户访问所请求的资源。
4. 控制器实现
在控制器中,需要提供一个/authenticate接口,用于验证用户身份并生成JWT token并返回给客户端。
以下是一个简单的控制器示例:
@RestController
@RequestMapping("/api")
public class AuthenticationController {
private AuthenticationManager authenticationManager;
@Autowired
public AuthenticationController(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@PostMapping("/authenticate")
public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
);
} catch (BadCredentialsException e) {
throw new Exception("Incorrect username or password", e);
}
final UserDetails userDetails = userDetailsService
.loadUserByUsername(authenticationRequest.getUsername());
final String jwt = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthenticationResponse(jwt));
}
}
在上面的控制器中,我们提供了一个/api/authenticate接口,用于验证用户身份。
接口接收一个AuthenticationRequest对象,包含用户名和密码。
如果验证成功,则使用jwtTokenUtil生成JWT token并将其添加到AuthenticationResponse对象中返回给客户端。
5. 示例
以下是一个示例代码:
AuthenticationRequest
public class AuthenticationRequest {
private String username;
private String password;
public AuthenticationRequest() {
}
public AuthenticationRequest(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
AuthenticationResponse
public class AuthenticationResponse {
private final String jwt;
public AuthenticationResponse(String jwt) {
this.jwt = jwt;
}
public String getToken() {
return jwt;
}
}
User
public class User {
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
测试用例
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class AuthenticationControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private UserRepository userRepository;
@Before
public void setUp() throws Exception {
userRepository.deleteAll();
userRepository.save(new User("admin", "password"));
}
@Test
public void should_return_jwt_token_when_authenticate_given_valid_username_and_password() throws Exception {
AuthenticationRequest authenticationRequest = new AuthenticationRequest("admin", "password");
mockMvc.perform(post("/api/authenticate")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(authenticationRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token").isString());
}
@Test
public void should_return_error_when_authenticate_given_invalid_username_and_password() throws Exception {
AuthenticationRequest authenticationRequest = new AuthenticationRequest("invalid_username", "invalid_password");
mockMvc.perform(post("/api/authenticate")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(authenticationRequest)))
.andExpect(status().isUnauthorized());
}
}
在测试用例中,我们使用MockMvc模拟一个HttpServletRequest并发送给/authenticate接口,使用ObjectMapper将AuthenticationRequest对象转换为JSON格式并将其传送给/authenticate接口。
如果验证成功,我们期望返回200 OK,并且返回的JSON对象应该包含一个名为token的字符串。如果验证失败,我们期望返回401 Unauthorized。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:SpringSecurity+JWT实现前后端分离的使用详解 - Python技术站