spring 如何设计一个好的 JWT 认证过滤器
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/41975045/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to design a good JWT authentication filter
提问by arunan
I am new to JWT. There isn't much information available in the web, since I came here as a last resort. I already developed a spring boot application using spring security using spring session. Now instead of spring session we are moving to JWT. I found few links and now I can able to authenticate a user and generate token. Now the difficult part is, I want to create a filter which will be authenticate every request to the server,
我是 JWT 的新手。网络上可用的信息不多,因为我是作为最后的手段来到这里的。我已经使用 spring session 使用 spring security 开发了一个 spring boot 应用程序。现在,我们将转向 JWT,而不是春季会议。我发现了几个链接,现在我可以验证用户并生成令牌。现在困难的部分是,我想创建一个过滤器来验证对服务器的每个请求,
- How will the filter validate the token? (Just validating the signature is enough?)
- If someone else stolen the token and make rest call, how will I verify that.
- How will I by-pass the login request in the filter? Since it doesn't have authorization header.
- 过滤器将如何验证令牌?(仅仅验证签名就足够了?)
- 如果其他人偷了令牌并拨打了休息电话,我将如何验证。
- 我将如何绕过过滤器中的登录请求?因为它没有授权标头。
采纳答案by Matthieu Saleta
Here is a filter that can do what you need :
这是一个可以满足您需求的过滤器:
public class JWTFilter extends GenericFilterBean {
private static final Logger LOGGER = LoggerFactory.getLogger(JWTFilter.class);
private final TokenProvider tokenProvider;
public JWTFilter(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException,
ServletException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String jwt = this.resolveToken(httpServletRequest);
if (StringUtils.hasText(jwt)) {
if (this.tokenProvider.validateToken(jwt)) {
Authentication authentication = this.tokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(servletRequest, servletResponse);
this.resetAuthenticationAfterRequest();
} catch (ExpiredJwtException eje) {
LOGGER.info("Security exception for user {} - {}", eje.getClaims().getSubject(), eje.getMessage());
((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
LOGGER.debug("Exception " + eje.getMessage(), eje);
}
}
private void resetAuthenticationAfterRequest() {
SecurityContextHolder.getContext().setAuthentication(null);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(SecurityConfiguration.AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
String jwt = bearerToken.substring(7, bearerToken.length());
return jwt;
}
return null;
}
}
And the inclusion of the filter in the filter chain :
并在过滤器链中包含过滤器:
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
public final static String AUTHORIZATION_HEADER = "Authorization";
@Autowired
private TokenProvider tokenProvider;
@Autowired
private AuthenticationProvider authenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(this.authenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
JWTFilter customFilter = new JWTFilter(this.tokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
// @formatter:off
http.authorizeRequests().antMatchers("/css/**").permitAll()
.antMatchers("/images/**").permitAll()
.antMatchers("/js/**").permitAll()
.antMatchers("/authenticate").permitAll()
.anyRequest().fullyAuthenticated()
.and().formLogin().loginPage("/login").failureUrl("/login?error").permitAll()
.and().logout().permitAll();
// @formatter:on
http.csrf().disable();
}
}
The TokenProvider class :
TokenProvider 类:
public class TokenProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(TokenProvider.class);
private static final String AUTHORITIES_KEY = "auth";
@Value("${spring.security.authentication.jwt.validity}")
private long tokenValidityInMilliSeconds;
@Value("${spring.security.authentication.jwt.secret}")
private String secretKey;
public String createToken(Authentication authentication) {
String authorities = authentication.getAuthorities().stream().map(authority -> authority.getAuthority()).collect(Collectors.joining(","));
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime expirationDateTime = now.plus(this.tokenValidityInMilliSeconds, ChronoUnit.MILLIS);
Date issueDate = Date.from(now.toInstant());
Date expirationDate = Date.from(expirationDateTime.toInstant());
return Jwts.builder().setSubject(authentication.getName()).claim(AUTHORITIES_KEY, authorities)
.signWith(SignatureAlgorithm.HS512, this.secretKey).setIssuedAt(issueDate).setExpiration(expirationDate).compact();
}
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(token).getBody();
Collection<? extends GrantedAuthority> authorities = Arrays.asList(claims.get(AUTHORITIES_KEY).toString().split(",")).stream()
.map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList());
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
LOGGER.info("Invalid JWT signature: " + e.getMessage());
LOGGER.debug("Exception " + e.getMessage(), e);
return false;
}
}
}
Now to answer your questions :
现在回答你的问题:
- Done in this filter
- Protect your HTTP request, use HTTPS
- Just permit all on the
/loginURI (/authenticatein my code)
- 在此过滤器中完成
- 保护您的 HTTP 请求,使用 HTTPS
- 只允许
/loginURI上的所有内容(/authenticate在我的代码中)
回答by pedrofb
I will focus in the general tips on JWT, without regarding code implemementation (see other answers)
我将专注于 JWT 的一般提示,而不涉及代码实现(参见其他答案)
How will the filter validate the token? (Just validating the signature is enough?)
过滤器将如何验证令牌?(仅仅验证签名就足够了?)
RFC7519 specifies how to validate a JWT (see 7.2. Validating a JWT), basically a syntactic validation and signature verification.
RFC7519 指定了如何验证 JWT(请参阅7.2. 验证 JWT),基本上是语法验证和签名验证。
If JWT is being used in an authentication flow, we can look at the validation proposed by OpenID connect specification 3.1.3.4 ID Token Validation. Summarizing:
如果在身份验证流程中使用 JWT,我们可以查看 OpenID connect 规范3.1.3.4 ID Token Validation提出的验证。总结:
isscontains the issuer identifier (andaudcontainsclient_idif using oauth)current time between
iatandexpValidate the signature of the token using the secret key
subidentifies a valid user
iss包含发行者标识符(如果使用 oauth 则aud包含client_id)之间的当前时间
iat和exp使用密钥验证令牌的签名
sub识别有效用户
If someone else stolen the token and make rest call, how will I verify that.
如果其他人偷了令牌并拨打了休息电话,我将如何验证。
Possesion of a JWT is the proof of authentication. An attacker who stoles a token can impersonate the user. So keep tokens secure
拥有 JWT 是身份验证的证明。窃取令牌的攻击者可以冒充用户。所以保持令牌安全
Encrypt communication channel using TLS
Use a secure storagefor your tokens. If using a web front-end consider to add extra security measures to protect localStorage/cookies against XSS or CSRF attacks
set short expiration timeon authentication tokens and require credentials if token is expired
使用 TLS加密通信通道
为您的令牌使用安全存储。如果使用 Web 前端,请考虑添加额外的安全措施以保护 localStorage/cookie 免受 XSS 或 CSRF 攻击
在身份验证令牌上设置较短的过期时间并在令牌过期时需要凭据
How will I by-pass the login request in the filter? Since it doesn't have authorization header.
我将如何绕过过滤器中的登录请求?因为它没有授权标头。
The login form does not require a JWT token because you are going to validate the user credential. Keep the form out of the scope of the filter. Issue the JWT after successful authentication and apply the authentication filter to the rest of services
登录表单不需要 JWT 令牌,因为您将要验证用户凭据。将表单保持在过滤器的范围之外。身份验证成功后发出 JWT 并将身份验证过滤器应用于其余服务
Then the filter should intercept all requestsexcept the login form, and check:
然后过滤器应该拦截除登录表单之外的所有请求,并检查:
if user authenticated? If not throw
401-Unauthorizedif user authorized to requested resource? If not throw
403-ForbiddenAccess allowed. Put user data in the context of request( e.g. using a ThreadLocal)
如果用户通过身份验证?如果不扔
401-Unauthorized如果用户授权请求的资源?如果不扔
403-Forbidden允许访问。将用户数据放在请求的上下文中(例如使用 ThreadLocal)
回答by Lazar Lazarov
Take a look at thisproject it is very good implemented and has the needed documentation.
看看这个项目,它的实现非常好,并且有所需的文档。
1. It the above project this is the only thing you need to validate the token and it is enough. Where tokenis the value of the Bearerinto the request header.
1. 在上面的项目中,这是验证令牌唯一需要的东西,这就足够了。进入请求头token的值在哪里Bearer。
try {
final Claims claims = Jwts.parser().setSigningKey("secretkey")
.parseClaimsJws(token).getBody();
request.setAttribute("claims", claims);
}
catch (final SignatureException e) {
throw new ServletException("Invalid token.");
}
2. Stealing the token is not so easy but in my experience you can protect yourself by creating a Spring session manually for every successfull log in. Also mapping the session unique ID and the Bearer value(the token) into a Map(creating a Bean for example with API scope).
2. 窃取令牌并不容易,但根据我的经验,您可以通过为每次成功登录手动创建 Spring 会话来保护自己。还将会话唯一 ID 和承载值(令牌)映射到Map(例如创建 Bean)具有 API 范围)。
@Component
public class SessionMapBean {
private Map<String, String> jwtSessionMap;
private Map<String, Boolean> sessionsForInvalidation;
public SessionMapBean() {
this.jwtSessionMap = new HashMap<String, String>();
this.sessionsForInvalidation = new HashMap<String, Boolean>();
}
public Map<String, String> getJwtSessionMap() {
return jwtSessionMap;
}
public void setJwtSessionMap(Map<String, String> jwtSessionMap) {
this.jwtSessionMap = jwtSessionMap;
}
public Map<String, Boolean> getSessionsForInvalidation() {
return sessionsForInvalidation;
}
public void setSessionsForInvalidation(Map<String, Boolean> sessionsForInvalidation) {
this.sessionsForInvalidation = sessionsForInvalidation;
}
}
This SessionMapBeanwill be available for all sessions. Now on every request you will not only verify the token but also you will check if he mathces the session (checking the request session id does matches the one stored into the SessionMapBean). Of course session ID can be also stolen so you need to secure the communication. Most common ways of stealing the session ID is Session Sniffing(or the Men in the middle) and Cross-site script attack. I will not go in more details about them you can read how to protect yourself from that kind of attacks.
这SessionMapBean将适用于所有会话。现在,在每个请求中,您不仅要验证令牌,还要检查他是否对会话进行了计算(检查请求会话 ID 是否与存储在 中的会话 ID 匹配SessionMapBean)。当然,会话 ID 也可能被盗,因此您需要确保通信安全。窃取会话 ID 的最常见方法是会话嗅探(或中间人)和跨站点脚本攻击。我不会详细介绍它们,您可以阅读如何保护自己免受此类攻击。
3.You can see it into the project I linked. Most simply the filter will validated all /api/*and you will login into a /user/loginfor example.
3.你可以在我链接的项目中看到它。最简单的过滤器将验证所有/api/*,您将登录到/user/login例如。

