序言
jwt有好處也有壞處,好處就是不用在去存這些session了,省空間,做分佈式會話so easy。但是我個人是比較不推薦你使用這個的。我舉例一下幾種缺點
- 無法滿足註銷場景
- 無法滿足修改密碼場景
- 無法滿足token續簽場景
煩人的不能到期,控制到期設置黑名單,還是用到了存儲比如redis,說實話這有點本末倒置,壞處不多說,如果不強求那麼多安全,還是可以使用jwt減少成本,快速開發。
代碼請參考 https://github.com/AutismSuperman/springsecurity-example
思路
只需要加入一個過濾器,保證他在認證過濾器的前面即可。這樣就可以做到在認證前對 token的一個效驗。
這樣就ok了,理解了就開始做。
實踐
切記引入
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
數據
首先準備User實體
@Data
public class User implements UserDetails {
private Long id;
private String userName;
private String password;
private List<String> roles;
public User(Long id, String userName, String password, List<String> roles) {
this.id = id;
this.userName = userName;
this.password = password;
this.roles = roles;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
return authorities;
}
@Override
public String getUsername() {
return userName;
}
@Override
public String getPassword() {
return password;
}
}
然後準備初始化user用戶數據
接口
public interface IUserService {
User findByUsername(String userName);
}
然後是實現類
@Service
public class UserServiceImpl implements IUserService {
private static final Set<User> users = new HashSet<>();
// 密碼123 md5加密
static {
users.add(new User(1L, "fulin", "1dc568b64c0f67e7a86c89a12fa5bd5f", Arrays.asList("admin", "docker")));
users.add(new User(1L, "xiaohan", "1dc568b64c0f67e7a86c89a12fa5bd5f", Arrays.asList("admin", "docker")));
users.add(new User(1L, "longlong", "1dc568b64c0f67e7a86c89a12fa5bd5f", Arrays.asList("admin", "docker")));
}
@Override
public User findByUsername(String userName) {
return users.stream().filter(o -> StringUtils.equals(o.getUsername(), userName)).findFirst().get();
}
}
userDetailService
@Service
public class UserService implements UserDetailsService {
final IUserService iUserService;
public UserService(IUserService iUserService) {
this.iUserService = iUserService;
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = iUserService.findByUsername(s);
if (user == null) {
throw new UsernameNotFoundException("用戶不存在");
}
return user;
}
}
jwt
準備一下jwt的配置
@Data
@ConfigurationProperties(prefix = "security.jwt")
public class SecurityProperties {
private JwtProperties jwt = new JwtProperties();
}
配置類
/**
* Jwt的基本配置
*/
@Data
public class JwtProperties {
/**
* 默認前面祕鑰
*/
private String secret = "defaultSecret";
/**
* token默認有效期時長,1小時
*/
private Long expiration = 3600L;
/**
* token默認有效期時長,1個半小時
*/
private Long refreshExpiration = 5400L;
/**
* token的唯一標記
*/
private String md5Key = "randomKey";
/**
* GET請求是否需要進行Authentication請求頭校驗,true:默認校驗;false:不攔截GET請求
*/
private boolean preventsGetMethod = true;
}
配置類別忘了開啓喲
@SpringBootApplication
@EnableConfigurationProperties(SecurityProperties.class)
public class SecuritySimpleJwtApplication {
public static void main(String[] args) {
SpringApplication.run(SecuritySimpleJwtApplication.class, args);
}
}
然後我根據jjwt封裝了一個util方便使用
/**
* Jwt Util
*/
@Component
public class JwtTokenUtil {
private final SecurityProperties securityProperties;
public JwtTokenUtil(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
/**
* 獲取用戶名從token中
*/
public String getUsernameFromToken(String token) {
return getClaimFromToken(token).getSubject();
}
/**
* 獲取jwt失效時間
*/
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token).getExpiration();
}
/**
* 獲取私有的jwt claim
*/
public String getPrivateClaimFromToken(String token, String key) {
return getClaimFromToken(token).get(key).toString();
}
/**
* 獲取md5 key從token中
*/
public String getMd5KeyFromToken(String token) {
return getPrivateClaimFromToken(token, securityProperties.getJwt().getMd5Key());
}
/**
* 獲取jwt的payload部分
*/
public Claims getClaimFromToken(String token) {
return Jwts.parser()
.setSigningKey(generalKey())
.parseClaimsJws(token)
.getBody();
}
/**
* <pre>
* 驗證token是否失效
* true:過期 false:沒過期
* </pre>
*/
public Boolean isTokenExpired(String token) {
try {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
} catch (ExpiredJwtException expiredJwtException) {
return true;
}
}
/**
* 生成token(通過用戶名和簽名時候用的隨機數)
*/
public String generateToken(String userName, String randomKey) {
Map<String, Object> claims = new HashMap<>();
claims.put(securityProperties.getJwt().getMd5Key(), randomKey);
return doGenerateToken(claims, userName);
}
/**
* 生成token(通過用戶名和簽名時候用的隨機數)
*/
public String generateRefreshToken(String userName, String randomKey) {
Map<String, Object> claims = new HashMap<>();
claims.put(securityProperties.getJwt().getMd5Key(), randomKey);
return doGenerateRefreshToken(claims, userName);
}
/**
* 由字符串生成加密key
*
* @return
*/
public SecretKey generalKey() {
byte[] encodedKey = Base64.decodeBase64(securityProperties.getJwt().getSecret());
return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
}
/**
* 生成token
*/
private String doGenerateToken(Map<String, Object> claims, String subject) {
final Date createdDate = new Date();
final Date expirationDate = new Date(createdDate.getTime() + securityProperties.getJwt().getExpiration() * 1000);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, generalKey())
.compact();
}
/**
* 生成token
*/
private String doGenerateRefreshToken(Map<String, Object> claims, String subject) {
final Date createdDate = new Date();
final Date expirationDate = new Date(createdDate.getTime() + securityProperties.getJwt().getRefreshExpiration() * 1000);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, generalKey())
.compact();
}
/**
* 獲取混淆MD5簽名用的隨機字符串
*/
public String getRandomKey() {
return getRandomString(6);
}
/**
* 獲取隨機位數的字符串
*/
public String getRandomString(int length) {
final String base = "abcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
/**
* 刷新token
*
* @param token:token
* @return
*/
public String refreshToken(String token, String randomKey) {
String refreshedToken;
try {
final Claims claims = getClaimFromToken(token);
refreshedToken = generateToken(claims.getSubject(), randomKey);
} catch (Exception e1) {
refreshedToken = null;
}
return refreshedToken;
}
}
頒發token
都準備齊全了,頒發token呢我們就放在成功處理器去做,這裏爲了讓jwt具有刷新功能,特意準備了一個具備刷新功能的 token refreshToken
@Slf4j
@Component
public class AppAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
final String randomKey = jwtTokenUtil.getRandomKey();
String username = ((UserDetails) authentication.getPrincipal()).getUsername();
log.info("username:{}", username);
//生產JWT 令牌
final String token = jwtTokenUtil.generateToken(username, randomKey);
final String refreshToken = jwtTokenUtil.generateRefreshToken(username, randomKey);
log.info("登錄成功!");
ModelMap modelMap = GenerateModelMap.generateMap(HttpStatus.OK.value(), "登陸成功");
modelMap.put("token", JwtAuthenticationTokenFilter.TOKEN_PREFIX + token);
modelMap.put("refreshToken", JwtAuthenticationTokenFilter.TOKEN_PREFIX + refreshToken);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(modelMap));
}
}
jwt過濾器
接下來就是最重要的我們自己定義一個jwt過濾器,這是我自己實現的代碼如下,這裏訪問/refreshToken
重新頒發一個token
和 refreshToken
`給前臺
/**
* JWT過濾器
* <p>
* OncePerRequestFilter,顧名思義,
* 它能夠確保在一次請求中只通過一次filter,而需要重複的執行。
*/
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final String HEADER_NAME = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private SecurityProperties securityProperties;
@Autowired
private UserService userService;
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("請求路徑:{},請求方式爲:{}", request.getRequestURI(), request.getMethod());
if (antPathMatcher.match("/favicon.ico", request.getRequestURI())) {
log.info("jwt不攔截此路徑:{},請求方式爲:{}", request.getRequestURI(), request.getMethod());
filterChain.doFilter(request, response);
return;
}
/*
* get請求是否需要進行Authentication請求頭校驗,true:默認校驗;false:不攔截GET請求
* 因爲get請求比較特殊
*/
if (!securityProperties.getJwt().isPreventsGetMethod()) {
if (Objects.equals(RequestMethod.GET.toString(), request.getMethod())) {
log.info("jwt不攔截此路徑因爲開啓了不攔截GET請求:{},請求方式爲:{}", request.getRequestURI(), request.getMethod());
filterChain.doFilter(request, response);
return;
}
}
/*
* 排除路徑,並且如果是options請求是cors跨域預請求,設置allow對應頭信息
* permitUrls可以自定義不需要驗證的url
*/
String[] permitUrls = {"/authentication"};
for (String permitUrl : permitUrls) {
if (antPathMatcher.match(permitUrl, request.getRequestURI())
|| Objects.equals(RequestMethod.OPTIONS.toString(), request.getMethod())) {
log.info("jwt不攔截此路徑:{},請求方式爲:{}", request.getRequestURI(), request.getMethod());
filterChain.doFilter(request, response);
return;
}
}
// 獲取請求頭Authorization
String authHeader = request.getHeader(HEADER_NAME);
if (StringUtils.isBlank(authHeader) || !authHeader.startsWith(TOKEN_PREFIX)) {
log.error("Authorization的開頭不是Bearer,Authorization===>{}", authHeader);
responseEntity(response, HttpStatus.UNAUTHORIZED.value(), "暫無權限!");
return;
}
// 截取token
String authToken = authHeader.substring(TOKEN_PREFIX.length());
//判斷token是否失效
if (jwtTokenUtil.isTokenExpired(authToken)) {
log.info("token已過期!");
responseEntity(response, HttpStatus.UNAUTHORIZED.value(), "token已過期!");
return;
}
String randomKey = jwtTokenUtil.getMd5KeyFromToken(authToken);
String username = jwtTokenUtil.getUsernameFromToken(authToken);
//如果訪問的是刷新Token的請求
if (antPathMatcher.match("/refreshToken", request.getRequestURI()) && Objects.equals(RequestMethod.POST.toString(), request.getMethod())) {
final String getRandomKey = jwtTokenUtil.getRandomKey();
refreshEntity(response, HttpStatus.OK.value(), jwtTokenUtil.generateToken(username, getRandomKey), jwtTokenUtil.refreshToken(authToken, jwtTokenUtil.getRandomKey()));
return;
}
/*
* 驗證token是否合法
*/
if (StringUtils.isBlank(username) || StringUtils.isBlank(randomKey)) {
log.info("username{}或randomKey{} 可能爲null!", username, randomKey);
responseEntity(response, HttpStatus.UNAUTHORIZED.value(), "暫無權限!");
return;
}
//獲得用戶名信息放入上下文中
if (SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// token過期時間
long tokenExpireTime = jwtTokenUtil.getExpirationDateFromToken(authToken).getTime();
// token還剩餘多少時間過期
long surplusExpireTime = (tokenExpireTime - System.currentTimeMillis()) / 1000;
log.info("Token剩餘時間:" + surplusExpireTime);
filterChain.doFilter(request, response);
}
private void responseEntity(HttpServletResponse response, Integer status, String message) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(status);
ModelMap modelMap = GenerateModelMap.generateMap(status, message);
try {
response.getWriter().write(JSON.toJSONString(modelMap));
} catch (IOException e) {
e.printStackTrace();
}
}
private void refreshEntity(HttpServletResponse response, Integer status, String token, String refreshToken) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(status);
ModelMap modelMap = new ModelMap();
modelMap.put("token", JwtAuthenticationTokenFilter.TOKEN_PREFIX + token);
modelMap.put("refreshToken", JwtAuthenticationTokenFilter.TOKEN_PREFIX + refreshToken);
try {
response.getWriter().write(JSON.toJSONString(modelMap));
} catch (IOException e) {
e.printStackTrace();
}
}
}
加入過濾鏈
最後呢吧過濾器加入到過濾連裏就可以啦
@Configuration
public class ValidateSecurityCoreConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private UserService userService;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(
new PasswordEncoder() {
@Override
public String encode(CharSequence rawPassword) {
return MD5Util.encode((String) rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5Util.encode((String) rawPassword));
}
});
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginProcessingUrl("/authentication")
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.and()
.csrf().disable();
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.authorizeRequests().antMatchers("/authentication").permitAll();
}
}
至此呢我們就實現了一個jwt的認證,我只是粗略的做了以下很多都沒考慮進去,大家根據大體思路可以自行擴展(比如token拉黑啥的)。
注意
此外在我的github代碼中還有一套關於redis處理jwt的(感覺有點本末倒置,還不如用sessionId那套配合Spring Session)
本博文是基於springboot2.x 和security 5 如果有什麼不對的請在下方留言。