在本文中,我将展示如何进行基于 Spring Boot 的 REST API进行鉴权。保护 REST API 以避免对公共 API 进行任何不必要的调用已成为一种趋势。我们将使用一些 Spring 引导功能来实现 Spring 安全,并使用 JSON WebTokens 进行授权。
JSON WebTokens,称为 JWT,用于为用户形成授权。这有助于我们构建安全的 API,而且易于扩展。在身份验证期间,返回一个 JSON Web 令牌。每当用户想要访问受保护的资源时,浏览器都必须在 Authorization 标头中随请求一起发送 JWT。这里要了解的一件事是保护 REST API 是一种很好的安全实践。
基本上,我们将展示
我将为我在这篇博文中创建的公司保护 REST API 。此 API 还包括缓存。用户将尝试访问/cachedemo/v1/companies/
并且由于 API 受到保护,他将得到如下响应:
现在我们将实现如何保护这个 API 以及在它被保护时如何访问它。
由于我们要为 API 添加授权,因此我们需要用户能够登录和发送凭据的位置。这些凭证将被验证并生成一个令牌。然后,此令牌将在对 API 调用的请求中传输。令牌将在我们将添加的 Spring 安全授权过滤器中进行验证。如果令牌有效,用户将能够访问 API。
package com.betterjavacode.models;
import javax.persistence.*;
import java.io.Serializable;
@Entity(name = "User")
@Table(name = "user")
public class User implements Serializable
{
public User()
{
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "username")
private String username;
@Column(name = "password")
private String password;
public long getId()
{
return id;
}
public void setId(long id)
{
this.id = id;
}
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;
}
}
我们将添加一个控制器,用户可以在其中注册 和 的详细username
信息password
。
package com.betterjavacode.resources;
import com.betterjavacode.models.User;
import com.betterjavacode.repositories.UserRepository;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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;
@RestController
@RequestMapping(value = "/cachedemo/v1/users")
public class UserController
{
private UserRepository userRepository;
private BCryptPasswordEncoder bCryptPasswordEncoder;
public UserController(UserRepository userRepository, BCryptPasswordEncoder bCryptPasswordEncoder)
{
this.userRepository = userRepository;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}
@PostMapping("/signup")
public void signUp(@RequestBody User user)
{
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
userRepository.save(user);
}
}
现在当我们向 POST 请求时/cachedemo/v1/users/signup
,一个用户将被保存在数据库中。Password
因为我们正在使用,所以用户将以加密格式保存BCryptPasswordEncoder
。我们将展示用户如何登录以创建令牌。
为了处理用户登录,我们将添加一个AuthenticationFilter
将添加到 FilterChain 中的,Spring boot 将适当地处理它的执行。该过滤器将如下所示:
package com.betterjavacode.SpringAppCache;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
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.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter
{
private AuthenticationManager authenticationManager;
public AuthenticationFilter(AuthenticationManager authenticationManager)
{
this.authenticationManager = authenticationManager;
setFilterProcessesUrl("/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException
{
try
{
com.betterjavacode.models.User creds = new ObjectMapper().readValue(request.getInputStream(), com.betterjavacode .models.User.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(creds.getUsername(), creds.getPassword(),new ArrayList<>()));
}
catch(IOException e)
{
throw new RuntimeException("Could not read request" + e);
}
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain, Authentication authentication)
{
String token = Jwts.builder()
.setSubject(((User) authentication.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + 864_000_000))
.signWith(SignatureAlgorithm.HS512, "SecretKeyToGenJWTs".getBytes())
.compact();
response.addHeader("Authorization","Bearer " + token);
}
}
基本上,用户将在请求中向以 /login 结尾的 URL 发送凭据。此过滤器将有助于对用户进行身份验证,如果身份验证成功,将在响应标头中添加一个带有授权密钥的令牌。
我们添加另一个过滤器 AuthorizationFilter 来验证我们之前通过 AuthenticationFilter 传递的令牌。该过滤器将如下所示:
package com.betterjavacode.SpringAppCache;
import io.jsonwebtoken.Jwts;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
public class AuthorizationFilter extends BasicAuthenticationFilter
{
public AuthorizationFilter(AuthenticationManager authenticationManager)
{
super(authenticationManager);
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException
{
String header = request.getHeader("Authorization");
if(header == null || !header.startsWith("Bearer"))
{
filterChain.doFilter(request,response);
return;
}
UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request,response);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request)
{
String token = request.getHeader("Authorization");
if(token != null)
{
String user = Jwts.parser().setSigningKey("SecretKeyToGenJWTs".getBytes())
.parseClaimsJws(token.replace("Bearer",""))
.getBody()
.getSubject();
if(user != null)
{
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}
如果令牌验证成功,则返回用户并将其分配给安全上下文。
为了启用 Spring 安全性,我们将添加一个带有注释的新类 WebSecurityConfiguration @EnableWebSecurity
。这个类将扩展标准WebSecurityConfigurerAdapter
。在这个类中,我们将限制我们的 API 并添加一些我们需要在没有任何授权令牌的情况下访问的白名单 URL。这将如下所示:
package com.betterjavacode.SpringAppCache;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter
{
private BCryptPasswordEncoder bCryptPasswordEncoder;
private UserDetailsService userDetailsService;
private static final String[] AUTH_WHITELIST = {
"/v2/api-docs",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**"
};
public WebSecurityConfiguration(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder)
{
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
this.userDetailsService = userDetailsService;
}
protected void configure(HttpSecurity httpSecurity) throws Exception
{
httpSecurity.cors().and().csrf().disable().authorizeRequests()
.antMatchers(AUTH_WHITELIST).permitAll()
.antMatchers(HttpMethod.POST, "/cachedemo/v1/users/signup").permitAll()
.anyRequest().authenticated()
.and().addFilter(new AuthenticationFilter(authenticationManager()))
.addFilter(new AuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception
{
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
@Bean
CorsConfigurationSource corsConfigurationSource()
{
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**",new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
在方法配置中,我们限制了大多数 API,只允许 Swagger URL 和注册 URL。我们还向 HttpSecurity 添加过滤器。我们将添加自己的UserDetailsServiceImpl
类来验证用户凭据。
package com.betterjavacode.services;
import com.betterjavacode.models.User;
import com.betterjavacode.repositories.UserRepository;
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.Component;
import java.util.Collections;
@Component
public class UserDetailsServiceImpl implements UserDetailsService
{
private UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository)
{
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
User user = userRepository.findByUsername(username);
if(user == null)
{
throw new UsernameNotFoundException(username);
}
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), Collections.emptyList());
}
}
完成所有代码更改后,现在我们已准备好创建用户、登录和访问受保护的 REST API。从上图中,用户在访问受保护的 API 时收到拒绝访问错误。为了演示这个,我已经用用户名test1
和密码 test@123 注册了一个用户。
登录的 POST 请求将为我们提供授权令牌作为响应。现在在我们的 GET 请求中使用此令牌来检索公司数据。此 GET 请求如下所示:
通过这种方式,我们展示了如何使用 JSON 网络令牌保护 REST API。