主要實(shí)現(xiàn)的功能是Login拿到token,再用token請(qǐng)求資源。關(guān)于登錄用戶名密碼驗(yàn)證這個(gè)在另一篇文章有提到(Spring Boot + Security實(shí)現(xiàn)簡(jiǎn)單驗(yàn)證登錄操作),這里就主要講token的生成,驗(yàn)證以及用戶具體權(quán)限的驗(yàn)證。
本例子功能如下圖:

1.引入Spring Security Jwt依賴。
<!--spring security -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2.創(chuàng)建JwtTokenProvider類提供生成以及驗(yàn)證token的方法。
生成token主要用到四個(gè)元素:
1)username token主要的標(biāo)志
2)expireTime token過(guò)期時(shí)間(xxx ms)
3)issuedDate token的創(chuàng)建時(shí)間
4)signWith token簽名,包括簽名方法和密鑰
這里的過(guò)期時(shí)間和密鑰配在properties文件里面,代碼通過(guò)@Value拿的。
jwtTokenSecret = Sayo
tokenExpiredMs = 604800000
@Component
@PropertySource("classpath:auth.properties")
public class AuthParameters {
private String jwtTokenSecret;
private long tokenExpiredMs;
public String getJwtTokenSecret() {
return jwtTokenSecret;
}
@Value("${jwtTokenSecret}")
public void setJwtTokenSecret(String jwtTokenSecret) {
this.jwtTokenSecret = jwtTokenSecret;
}
public long getTokenExpiredMs() {
return tokenExpiredMs;
}
@Value("${tokenExpiredMs}")
public void setTokenExpiredMs(long tokenExpiredMs) {
this.tokenExpiredMs = tokenExpiredMs;
}
}
驗(yàn)證token用同樣的密鑰去解開(kāi)token
倘若能解開(kāi)則表示該token是合法可用的,解析時(shí)有可能會(huì)拋出以下5個(gè)exception,可以分別catch處理log出日志,這里都統(tǒng)一處理了。
1)ExpiredJwtException token時(shí)效過(guò)期異常
2)UnsupportedJwtException 驗(yàn)證的token和期待的token格式不一樣時(shí),例如解析的是一個(gè)明文JWT而期待的是一個(gè)加密簽名JWT時(shí)就會(huì)拋出這個(gè)異常。
3)MalformedJwtException 表示這不是一個(gè)正確方法創(chuàng)建的token。
4)SignatureException token簽名驗(yàn)證失敗異常
5)IllegalArgumentException token為null或者空異常
@Component
public class JwtTokenProvider {
Loggerlogger = LoggerFactory.getLogger(JwtTokenProvider.class);
@Autowired
private AuthParametersauthParameters;
/**
* Generate token for user login.
*
* @param authentication
* @return return a token string.
*/
public String createJwtToken(Authentication authentication) {
//user name
String username = ((org.springframework.security.core.userdetails.User) authentication.getPrincipal()).getUsername();
//expire time
Date expireTime =new Date(System.currentTimeMillis()+authParameters.getTokenExpiredMs());
//create token
String token = Jwts.builder()
.setSubject(username)
.setExpiration(expireTime)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS512, authParameters.getJwtTokenSecret())
.compact();
return token;
}
/**
* validate token eligible.
* if Jwts can parse the token string and no throw any exception, then the token is eligible.
* @param token a jws string.
*/
public boolean validateToken(String token) {
String VALIDATE_FAILED ="validate failed : ";
try {
Jwts.parser()
.setSigningKey(authParameters.getJwtTokenSecret())
.parseClaimsJws(token);
return true;
}catch (Exception ex) {
//ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, //IllegalArgumentException
logger.error(VALIDATE_FAILED + ex.getMessage());
return false;
}
}
}
3.創(chuàng)建JwtAuthenticationFilter類在用戶獲取資源之前讓spring去filter這個(gè)token是否合法可用。
繼承OncePerRequestFilter重寫(xiě)doFilterInternal方法,前端發(fā)送請(qǐng)求時(shí),token會(huì)放在header,在每個(gè)請(qǐng)求讀取資源之前后臺(tái)對(duì)token進(jìn)行filter。
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private AuthParameters authParameters;
@Autowired
private UserService userService;
//1.從每個(gè)請(qǐng)求header獲取token
//2.調(diào)用前面寫(xiě)的validateToken方法對(duì)token進(jìn)行合法性驗(yàn)證
//3.解析得到username,并從database取出用戶相關(guān)信息權(quán)限
//4.把用戶信息(role等)以UserDetail形式放進(jìn)SecurityContext以備整個(gè)請(qǐng)求過(guò)程使用。
// (例如哪里需要判斷用戶權(quán)限是否足夠時(shí)可以直接從SecurityContext取出去check)
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = getJwtFromRequest(request);
if (token != null && jwtTokenProvider.validateToken(token)) {
String username = getUsernameFromJwt(token, authParameters.getJwtTokenSecret());
UserDetails userDetails = userService.getUserDetailByUserName(username);
Authentication authentication = new UsernamePasswordAuthenticationToken(
userDetails,null,userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
logger.error(request.getParameter("username") + " :Token is null");
}
super.doFilter(request, response, filterChain);
}
/**
* Get Bear jwt from request header Authorization.
*
* @param request servlet request.
* @return token or null.
*/
private String getJwtFromRequest(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer")) {
return token.replace("Bearer ", "");
}
return null;
}
/**
* Get user name from Jwt, the user name have set to jwt when generate token.
*
* @param token jwt token.
* @param signKey jwt sign key, set in properties file.
* @return user name.
*/
private String getUsernameFromJwt(String token, String signKey) {
return Jwts.parser().setSigningKey(signKey)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
上面調(diào)用到的getUserDetailByUserName在UserService
/**
* Get {@link UserDetails} by user name.
* @return
*/
@Transactional
public UserDetails getUserDetailByUserName(String username){
User user = this.userRepository.findByUserName(username);
if(user == null){
//throw exception inform front end not this user
throw new UsernameNotFoundException("user + " + username + "not found.");
}
List<String> roleList = this.userRepository.queryUserOwnedRoleCodes(username);
List<GrantedAuthority> authorities = roleList.stream()
.map(role -> new SimpleGrantedAuthority(role)).collect(Collectors.toList());
return new org.springframework.security.core.userdetails
.User(username,user.getPassword(),authorities);
}
4.配置HttpSecurity
其他配置說(shuō)明在文章開(kāi)頭提到的另一篇文章中有寫(xiě),這里只說(shuō)新添加的配置
1)添加注解@EnableGlobalMethodSecurity,并設(shè)置prePostEnabled為true(默認(rèn)是false),啟用Spring security的前注解(例如本例用到的@PreAuthorize)
2)把自定義的JwtAuthenticationFilter添加到UsernamePasswordAuthenticationFilter之前。
3)因?yàn)槲覀兪褂昧藅oken,所以session要禁止掉創(chuàng)建和使用,不然會(huì)白白耗掉很多空間,SessionCreationPolicy設(shè)為STATELESS,即永不創(chuàng)建HttpSession并且不會(huì)使用HttpSession去獲取SecurityContext。


5.登陸成功在AuthenticationSuccessHandler返回token給前端
@Service("authenticationSuccessHandler")
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private JwtTokenProvider tokenProvider;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response
, Authentication authentication) throws IOException {
logger.info("User: " + authentication.getName() + " Login successfully.");
this.returnJson(response,authentication);
}
private void returnJson(HttpServletResponse response,Authentication authentication) throws IOException {
response.setStatus(HttpServletResponse.SC_OK);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter()
.println("{\"tokenType\":\"Bearer\",\"token\": \"" + tokenProvider.createJwtToken(authentication) + "\"}");
}
}
6.在Controller方法加上具體權(quán)限限制
用@PreAuthorize("hasAuthority('role')"),進(jìn)行方法級(jí)別驗(yàn)證登錄user的是否有足夠的權(quán)限訪問(wèn)該方法,這里舉例用的是admin權(quán)限。
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
private UserService userService;
@GetMapping(value = "/user")
@PreAuthorize("hasAuthority('admin')")
public UserView getUserByName(@RequestParam("userName") String userName) {
return userService.getUserByUserName(userName);
}
}
hasAuthority在spring中的源碼,主要是在authentication中拿到當(dāng)前user所擁有的role然后再check是否包含有訪問(wèn)這個(gè)方法需要的role。
public final boolean hasAuthority(String authority) {
return hasAnyAuthority(authority);
}
public final boolean hasAnyAuthority(String... authorities) {
return hasAnyAuthorityName(null, authorities);
}
private boolean hasAnyAuthorityName(String prefix, String... roles) {
Set<String> roleSet = getAuthoritySet();
for (String role : roles) {
String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
if (roleSet.contains(defaultedRole)) {
return true;
}
}
return false;
}
7. 測(cè)試
先看看我數(shù)據(jù)庫(kù)的數(shù)據(jù)是這樣的。



登陸帶admin權(quán)限的用戶,成功獲取資源
1)登錄

2)請(qǐng)求資源在Header帶上key為Authorization,value為Bearer +token,因?yàn)楫?dāng)前登錄的用戶Sayo在數(shù)據(jù)庫(kù)是帶有admin權(quán)限所以成功獲得數(shù)據(jù)。

登錄不帶admin權(quán)限的用戶,無(wú)法獲取資源,返回權(quán)限不夠提示
1)登錄

2)當(dāng)前用戶不帶admin權(quán)限,而該方法配置了需要admin權(quán)限訪問(wèn),請(qǐng)求資源失敗
