1.主要原理:首先登录,如果登录成功,则在响应的header中加入生成的jwt token。
然后客户在发起请求时,校验header中的token是否合法,如果合法放行,不合法则返回错误信息
2.依赖
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
io.jsonwebtoken
jjwt
0.9.1
com.alibaba
fastjson
1.2.56
org.apache.commons
commons-lang3
3.9
3.登录过滤器,生成token
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.StreamUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.Charset;
public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {
static final String TOKEN_PREFIX = "Bearer";
static final String HEADER_STRING = "Authorization";
private Logger log = LoggerFactory.getLogger(JWTLoginFilter.class);
public JWTLoginFilter(AuthenticationManager authManager) {
//配置只有访问/api/login时,这个过滤器才会起作用
super(new AntPathRequestMatcher("/api/login", "POST"));
setAuthenticationManager(authManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
JSONObject params;
try {
String jsonparam = StreamUtils.copyToString(request.getInputStream(), Charset.forName("utf-8"));
params = JSONObject.parseObject(jsonparam);
} catch (IOException e) {
log.error(e.getMessage(), e);
throw new AuthenticationServiceException("读取登录请求参数异常");
}
String username = null;
String password = null;
if (params != null) {
username = params.getString("username");
password = params.getString("password");
}
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
return this.getAuthenticationManager().authenticate(authRequest);
}
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException {
TokenAuthenticationHandler tokenAuthenticationHandler = new TokenAuthenticationHandler();
Object obj = auth.getPrincipal();
if (obj != null) {
UserDetails userDetails = (UserDetails) obj;
String token = tokenAuthenticationHandler.generateToken(JSONObject.toJSONString(userDetails));
res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + token);
//todo 将最新生成的token保存在redis或是数据库中,可以在JWTAuthenticationFilter中判断提交上来的token和当前数据库中的token是否一致
//todo ,如果不一致则说明客户端没有使用最新的token
}
}
}
4.验证token过滤器
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JWTAuthenticationFilter extends GenericFilterBean {
static final String HEADER_STRING = "Authorization";
static final String TOKEN_PREFIX = "Bearer";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader(HEADER_STRING);
if (StringUtils.isBlank(token) && req.getRequestURI().startsWith("/api")) {
HttpServletResponse res = (HttpServletResponse) response;
res.addHeader("Content-Type", "text/html;charset=UTF-8");
res.setStatus(401);
res.getWriter().write("{\"error\":\"请提供授权码\"}");
return;
}
if (StringUtils.isNotBlank(token) && token.startsWith(TOKEN_PREFIX)) {
//todo 从数据库中查询JWTLoginFilter生成的最新token,与当前提交的token对比,如果不一致,则返回token过期错误提示
TokenAuthenticationHandler tokenAuthenticationHandler = new TokenAuthenticationHandler();
String subject = tokenAuthenticationHandler.getSubjectFromToken(token.replace(TOKEN_PREFIX, ""));
if (StringUtils.isBlank(subject)) {
HttpServletResponse res = (HttpServletResponse) response;
res.addHeader("Content-Type", "text/html;charset=UTF-8");
res.setStatus(401);
res.getWriter().write("{\"error\":\"授权码错误\"}");
return;
} else {
//todo 校验token成功后,如果需要每次访问更新token,可以再次重新生成token,添加到响应头中
//保存登录信息
SecurityContextHolder.getContext().setAuthentication(new JWTAuthenticationToken(subject));
}
}
filterChain.doFilter(request, response);
}
}
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import static java.util.Collections.emptyList;
public class JWTAuthenticationToken extends UsernamePasswordAuthenticationToken {
public JWTAuthenticationToken(Object principal) {
super(principal,null,emptyList());
}
@Override
public Object getCredentials() {
return super.getCredentials();
}
@Override
public Object getPrincipal() {
return super.getPrincipal();
}
}
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class TokenAuthenticationHandler {
private static final String CLAIM_KEY_CREATED = "created";
private static final String CLAIM_KEY_SUBJECT = "subject";
private static final String DEFAULT_SECRET = "@*secret@*";
//有效期3天
private static final Long DEFAULT_EXPIRATION = 259200L;
public TokenAuthenticationHandler() {
}
public String getSubjectFromToken(String token) {
String subject;
try {
final Claims claims = getClaimsFromToken(token);
subject = claims.get(CLAIM_KEY_SUBJECT).toString();
} catch (Exception e) {
subject = null;
}
return subject;
}
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(DEFAULT_SECRET).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + DEFAULT_EXPIRATION * 1000);
}
public String generateToken(String subject) {
Map claims = new HashMap();
claims.put(CLAIM_KEY_CREATED, new Date());
claims.put(CLAIM_KEY_SUBJECT, subject);
return generateToken(claims);
}
public String generateToken(Map claims) {
return Jwts.builder().setClaims(claims).setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, DEFAULT_SECRET).compact();
}
}
5.springsecurity配置
import com.example.demo.conf.security.JWTAuthenticationFilter;
import com.example.demo.conf.security.JWTLoginFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(passwordEncoder())
.withUser("zhangsan").password("123456").roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.formLogin().loginPage("/toLogin")//自定义登录页面
.loginProcessingUrl("/login")//登录提交表单url
.defaultSuccessUrl("/index")//登录成功后页面
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.addHeader("Content-Type", "text/html;charset=UTF-8");
if ("/api/login".equals(httpServletRequest.getRequestURI().toString())) {
httpServletResponse.getWriter().write("{\"error\":\"登录失败\"}");
} else {
httpServletResponse.getWriter().write("登录失败");
}
}
})//登录失败处理
.and().authorizeRequests()
.antMatchers("/login").permitAll()//放行登录提交表单url
.antMatchers("/toLogin").permitAll()//放行登录界面url
.antMatchers("/api/login").permitAll()//放行api登录,获得jwt授权码
.antMatchers("/**").authenticated() //所有路径都需要授权验证
.and()
.addFilterBefore(loginFilter(), UsernamePasswordAuthenticationFilter.class)//api登录处理逻辑过滤器
.addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);//验证token是否合法过滤器
}
public JWTLoginFilter loginFilter() throws Exception {
JWTLoginFilter loginFilter = new JWTLoginFilter(authenticationManager());
loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
response.setContentType("application/json");
response.getWriter().write("{\"errormsg\":\"jwt认证失败\"}");
});
loginFilter.setContinueChainBeforeSuccessfulAuthentication(false);
return loginFilter;
}
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return charSequence.toString().equals(s);
}
};
}
}
项目源码地址:https://github.com/394938226/jwt_token