在本文中,我将展示如何进行基于 Spring Boot 的 REST API进行鉴权。保护 REST API 以避免对公共 API 进行任何不必要的调用已成为一种趋势。我们将使用一些 Spring 引导功能来实现 Spring 安全,并使用 JSON WebTokens 进行授权。
这种情况下的用户流是
- 用户登录
- 我们验证用户凭据
- 令牌被发送回用户代理。
- 用户尝试访问受保护的资源。
- 用户在访问受保护资源时发送 JWT。我们验证 JWT。
- 如果 JWT 有效,我们允许用户访问该资源。
JSON WebTokens,称为 JWT,用于为用户形成授权。这有助于我们构建安全的 API,而且易于扩展。在身份验证期间,返回一个 JSON Web 令牌。每当用户想要访问受保护的资源时,浏览器都必须在 Authorization 标头中随请求一起发送 JWT。这里要了解的一件事是保护 REST API 是一种很好的安全实践。
基本上,我们将展示
- 验证 JSON WebToken
- 验证签名
- 检查客户端权限
前置准备
- Java 8,
- 数据库
- IntelliJ 编辑器
- Gradle
基于 Spring Boot 的 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。