这篇文章将使用JWT(JSOn Web Token)认证来保护我们的REST API接口。我们基于spring boot和maven上来构建并让我们的API处于保护之中,我们将API的注册(添加用于生成token的用户信息接口)和token生成接口分离开来。我们通过扩展于OncePerRequestFilter类来实现自定义JWT验证机制。这种认证机制可以用于URL以及method上。最后,我们将使用google的REST client工具来测试这些功能。这是另一个使用 Spring Boot Security Oauth2 JWT Token 方式来使我们的REST API处于保护之中的。
什么是JWT
JWT(JSON Web Token)是一个开放标准(RFC7519),它定义了一种紧凑且独立的方式,用于在各方之间作为JSON对象安全地传输信息。一种无状态认证机制,因为用户状态永远不会保存在服务器内存中。 JWT令牌由3个部分组成,用点(.)分隔,即Header.payload.signature。
Header(头) 由token的类型和hash算法组成。这个JSON结构由这两个key组成并最后用Base64Encoded算法进行加密。
{ "alg": "HS256", "typ": "JWT" }
Payload(载体) 包含这些东西。主的要有三种类型: reserved, public, and private 。Reserved 区是预先定义好的,比如iss(发行者),exp(过期时间),sub(主题),aud(观众)。在 private区,你可以创建你自己的内容,比如subject,role或者其他的东西。
"sub": "Alex123",
"scopes": [
{
"authority": "ROLE_ADMIN"
}
],
"iss": "[http://devglan.com](http://devglan.com)",
"iat": 1508607322,
"exp": 1508625322
}
Signature(签名)是确保令牌在传输途中不会被更改。比如要使用HMAC SHA256算法,它将按下面的方式创建。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
服务器将检查请求(访问受保护的路由)的Header中只是否有Authorization字段并确认是有效的JWT,如果都是Ok的,那么用户可以被请允许访问受保护的资源。每当用户想要访问受保护的路或者资源时,那么你就得将Authorization添加到请求头中并使用Bearer 模式(schema)。这个header的内容应该像下面的这样:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBbGV4MTIzIiwic2N.v9A80eU1VDo2Mm9UqN2FyEpyT79IUmhg
项目结构:
以下是我们以Spring Boot JWT身份验证构建的最终项目结构。
(图挂了)
JWT认证机制
自定义一个JwtAuthenticationFilter类并继承于OncePerRequestFilter,主要的目的就是确定每个请求都会被他处理。这个类会检查带有authorization header的请求并验证他们的正确性然后将认证信息设置到上下文中。关于哪些资源须要保护哪些资源不须要保护的配置我们在WebSecurityConfig.java中配置。
JwtAuthenticationFilter.java
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
String header = req.getHeader(HEADER_STRING);
String username = null;
String authToken = null;
if (header != null && header.startsWith(TOKEN_PREFIX)) {
authToken = header.replace(TOKEN_PREFIX,"");
try {
username = jwtTokenUtil.getUsernameFromToken(authToken);
} catch (IllegalArgumentException e) {
logger.error("an error occured during getting username from token", e);
} catch (ExpiredJwtException e) {
logger.warn("the token is expired and not valid anymore", e);
} catch(SignatureException e){
logger.error("Authentication Failed. Username or Password not valid.");
}
} else {
logger.warn("couldn't find bearer string, will ignore the header");
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
logger.info("authenticated user " + username + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(req, res);
}
}
接下来的这个util类主要是生成token和从token中提取username信息.我们须要将paht为/token/* 和 /signup/*的url暴露出来,其他url则受保护起来。
JwtTokenUtil.java
@Component
public class JwtTokenUtil implements Serializable {
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public T getClaimFromToken(String token, Function claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(SIGNING_KEY)
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public String generateToken(User user) {
return doGenerateToken(user.getUsername());
}
private String doGenerateToken(String subject) {
Claims claims = Jwts.claims().setSubject(subject);
claims.put("scopes", Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
return Jwts.builder()
.setClaims(claims)
.setIssuer("[http://devglan.com](http://devglan.com)")
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY_SECONDS*1000))
.signWith(SignatureAlgorithm.HS256, SIGNING_KEY)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (
username.equals(userDetails.getUsername())
&& !isTokenExpired(token));
}
}
接下来的这些常量就是我们要实现上述功能所使用到的常量。
Constants.java
public class Constants {
public static final long ACCESS_TOKEN_VALIDITY_SECONDS = 5*60*60;
public static final String SIGNING_KEY = "devglan123r";
public static final String TOKEN_PREFIX = "Bearer ";
public static final String HEADER_STRING = "Authorization";
}
Spring Boot Security配置
我们现在就开始动手配置spring boot security的配置吧。我们须要注入userDetailsService,以便我们从数据库中获取用户认证信息。
@EnableGlobalMethodSecurity注解的安全性控制可以细化到方法级别,您可以在方法上使用@Secured注解,以在方法级别提供基于角色的身份验证。
WebSecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource(name = "userService")
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(encoder());
}
@Bean
public JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationFilter();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().
authorizeRequests()
.antMatchers("/token/*").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public BCryptPasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
}
接下来的这个Controller主要是通过user来用于生成token信息。如果您在WebSecurityConfig.java中注意到,我们已将此URL配置为不进行身份验证,以便用户可以生成具有有效凭据的JWT令牌。
AuthenticationController.java
@RestController
@RequestMapping("/token")
public class AuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserService userService;
@RequestMapping(value = "/generate-token", method = RequestMethod.POST)
public ResponseEntity register(@RequestBody LoginUser loginUser) throws AuthenticationException {
final Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginUser.getUsername(),
loginUser.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
final User user = userService.findOne(loginUser.getUsername());
final String token = jwtTokenUtil.generateToken(user);
return ResponseEntity.ok(new AuthToken(token));
}
}
接下来我们创建一些非常简单的REST API来做为测试用。他们的代码如下:
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value="/user", method = RequestMethod.GET)
public List listUser(){
return userService.findAll();
}
@RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
public User getOne(@PathVariable(value = "id") Long id){
return userService.findById(id);
}
}
接下来就是我们的实体对象:
@Entity
public class User {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private long id;
@Column
private String username;
@Column
@JsonIgnore
private String password;
@Column
private long salary;
@Column
private int age;
}
默认脚本:
在我们的应用启动之前我们须要将这些脚本写入我们的db之中.
INSERT INTO User (id, username, password, salary, age) VALUES (1, 'Alex123', '$2a$04$I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu', 3456, 33);
INSERT INTO User (id, username, password, salary, age) VALUES (2, 'Tom234', '$2a$04$PCIX2hYrve38M7eOcqAbCO9UqjYg7gfFNpKsinAxh99nms9e.8HwK', 7823, 23);
INSERT INTO User (id, username, password, salary, age) VALUES (3, 'Adam', '$2a$04$I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu', 4234, 45);
用户的JWT注册:
由于我们有默认脚本来预先填充数据库中的数据以用于我们的测试目的,但我们也可以公开API以供用户注册。 使用此API用户可以注册并使用相同的用户名和密码来生成令牌。 为此,我们在Controller类中添加了以下方法。
@RequestMapping(value="/signup", method = RequestMethod.POST)
public User saveUser(@RequestBody UserDto user){
return userService.save(user);
}
一旦添加,我们需要删除对此URL的访问限制以供公共访问。为此,请在我们的spring boot security配置中添加以下行。
.antMatchers("/token/*", "/signup").permitAll()
现在要创建用户了,我们在UserServiceImpl.java中有简单的实现来保存数据库中的用户记录。这里要注意的是使用bcrypt encoder来对密码进行加密。 我们已经在WebSecurityConfig.java中配置了相同的编码器。
@Autowired
private BCryptPasswordEncoder bcryptEncoder;
@Override
public User save(UserDto user) {
User newUser = new User();
newUser.setUsername(user.getUsername());
newUser.setPassword(bcryptEncoder.encode(user.getPassword()));
newUser.setAge(user.getAge());
newUser.setSalary(user.getSalary());
return userDao.save(newUser);
}
测试这个应用:
我们将使用Advanced REST Client来测试spring boot jwt身份验证。
总结:
我希望这篇文章能为您提供所需的信息。 这篇文章的源代码可以在github(https://github.com/only2dhir/spring-boot-jwt)上找到。如果您有任何想要添加或分享的内容,请在评论部分下面分享。
原文地址:https://www.devglan.com/spring-security/spring-boot-jwt-auth