在这篇文章中,我们将通过JWT(JSOn Web Token)认证来保护我们的REST API 。我们将使用基于spring boot maven的配置来开发并保护我们的API,并提供单独的API用于注册并生成令牌。我们将扩展OncePerRequestFilter类,以使用JWT定义我们的自定义认证机制。认证机制可以应用于URL和方法。最后,我们将使用谷歌高级REST客户端测试实现。
以下类继承了OncePerRequestFilter,确保每个请求调度都有一次执行。该类检查授权头并验证JWT令牌并在上下文中设置验证。这样做可以保护我们的API免受那些没有任何授权令牌的请求。有关哪些资源需要保护以及哪些不可以配置WebSecurityConfig.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类。这里是我们需要url的配置,例如/ token / *和/ signup / *是公开可用的,其余url是受限于公共访问。@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")
.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));
}
}
以下是我们在上面的实现中使用的常量。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引导安全配置。我们注入了userDetailsService以从数据库中获取用户凭据。
在这里,注释@EnableGlobalMethodSecurity
启用了方法级别的安全性,您可以使用注释(例如@Secured)注释您的方法,以在方法级别提供基于角色的身份验证。
@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();
}
}
以下是暴露在代表用户创建令牌的控制器,如果您注意到WebSecurityConfig.java
我们已将此url配置为不进行身份验证,以便用户可以使用有效凭据生成JWT令牌。
@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 Apis公开测试用途。以下是实现。
@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;
}
以下是在应用程序启动时插入的插入语句。
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);
@RequestMapping(value="/signup", method = RequestMethod.POST)
public User saveUser(@RequestBody UserDto user){
return userService.save(user);
}
一旦添加,我们需要删除此URL的限制以供公众访问。为此,请在我们的Spring Boot security config中添加以下行。
.antMatchers("/token/*", "/signup").permitAll()
现在为了创建用户,我们在UserServiceImpl.java
保存数据库中的用户记录方面做了简单的实现。这里要注意的一点是使用bcrypt编码器的密码加密。我们已经自动装配了我们定义为bean的相同编码器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);
}
这将显示用户注册过程的以下URL以生成JWT令牌。
我们将使用Advanced REST Client来测试spring boot jwt认证。
使用令牌生成AuthToken 访问资源,而不使用令牌访问资源
http://blog.xqlee.com/article/384.html