Spring Boot整合JWT&Spring Security進行介面安全認證
阿新 • • 發佈:2018-12-30
一,協議
token驗證未通過
返回:
{
"header": {
"errorinfo": "無效的token",
"errorcode": "8001"
}
}
頁面上對這種情況的處理,都跳轉到登陸頁面;
登陸驗證未通過
返回:
{
"header": {
"errorinfo": "使用者名稱或密碼錯誤,請重新輸入!",
"errorcode": "8002"
}
}
前端頁面對這種情況的處理,清空使用者名稱和密碼,重新輸入;
其他正常情況
按照資料介面的定義,正常互動,參考系統介面協議定義;
JWT
參考文件JWT文件
後端配置
application.yml
# JWT 認證配置
jwt:
header: Authorization
secret: w-oasis123456
expiration: 604800 #token七天不過期
tokenHead: "Bearer "
exceptUrl: "/auth/**"
使用者認證相關:
自定義JwtUser,實現spring security 的UserDetails類,用於使用者的認證:
/**
* \* Created: liuhuichao
* \* Date: 2017/10/30
* \* Time: 上午10:32
* \* Description: 為了安全服務的User
* \
*/
public class JwtUser implements UserDetails {
private final Long id;
private final String username; //設定為account
private final String password;
private final Collection<? extends GrantedAuthority> authorities;
public JwtUser(Long id, String username, String password, Collection<? extends GrantedAuthority> authorities) {
this .id = id;
this.username = username;
this.password = password;
this.authorities = authorities;
}
/**
* 返回分配給使用者的角色列表
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
/**
* 賬戶是否未過期
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 賬戶是否未鎖定
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 密碼是否未過期
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 賬戶是否啟用
* @return
*/
@Override
public boolean isEnabled() {
return true;
}
}
自定義類,實現UserDetailsService 的認證方法:
/**
* \* Created: liuhuichao
* \* Date: 2017/10/30
* \* Time: 上午10:54
* \* Description: 提供一種從使用者名稱可以查到使用者並返回的方
* \
*/
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
/**
* 提供一種從使用者名稱可以查到使用者並返回的方法【本系統使用手機號account進行唯一使用者驗證】
* @param account
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
/**TODO:此處需要寫明從使用者表裡面跟根據使用者account查詢使用者的方法**/
User user =new User();
user.setAccount("17319237587");
user.setPwd("123");
user.setUserId(1L);
List<String> roles=new ArrayList<>();
roles.add("ADMIN");
user.setRoles(roles);
return JwtUserFactory.create(user);
}
}
配置資料庫實體類跟認證類:
/**
* \* Created: liuhuichao
* \* Date: 2017/10/30
* \* Time: 上午10:43
* \* Description: factory:根據User建立JwtUser
* \
*/
public final class JwtUserFactory {
private JwtUserFactory() {
}
public static JwtUser create(User user) {
return new JwtUser(
user.getUserId(),
user.getAccount(),//account是唯一的
user.getPwd(),
mapToGrantedAuthorities(user.getRoles())
);
}
private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) {
return authorities.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
token
token操作類:
/**
* \* Created: liuhuichao
* \* Date: 2017/10/27
* \* Time: 下午3:12
* \* Description:
* \
*/
@Component
public class JwtUtil {
private static final String CLAIM_KEY_USER_ACCOUNT = "sub";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret; //祕鑰
@Value("${jwt.expiration}")
private Long expiration; //過期時間
/**
* 從token中獲取使用者account
* @param token
* @return
*/
public String getUserAccountFromToken(String token) {
String useraccount;
try {
final Claims claims = getClaimsFromToken(token);
useraccount = claims.getSubject();
} catch (Exception e) {
useraccount = null;
}
return useraccount;
}
/**
* 從token中獲取建立時間
* @param token
* @return
*/
public Date getCreatedDateFromToken(String token) {
Date created;
try {
final Claims claims = getClaimsFromToken(token);
created = new Date((Long) claims.get(CLAIM_KEY_CREATED));
} catch (Exception e) {
created = null;
}
return created;
}
/**
* 獲取token的過期時間
* @param token
* @return
*/
public Date getExpirationDateFromToken(String token) {
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}
/**
* 從token中獲取claims
* @param token
* @return
*/
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 生存token的過期時間
* @return
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 判斷token是否過期
* @param token
* @return
*/
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
Boolean result= expiration.before(new Date());
return result;
}
/**
* 生成token
* @param userDetails
* @return
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USER_ACCOUNT, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* token 是否可重新整理
* @param token
* @return
*/
public Boolean canTokenBeRefreshed(String token) {
final Date created = getCreatedDateFromToken(token);
return !isTokenExpired(token);
}
/**
* 重新整理token
* @param token
* @return
*/
public String refreshToken(String token) {
String refreshedToken;
try {
final Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 驗證token
* @param token
* @param userDetails
* @return
*/
public Boolean validateToken(String token, UserDetails userDetails) {
JwtUser user = (JwtUser) userDetails;
final String useraccount = getUserAccountFromToken(token);
final Date created = getCreatedDateFromToken(token);
Boolean result= (
useraccount.equals(user.getUsername())
&& !isTokenExpired(token)
);
return result;
}
}
驗證token的filter配置:
/**
* \* Created: liuhuichao
* \* Date: 2017/10/30
* \* Time: 上午11:23
* \* Description: 驗證token
* \
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Value("${jwt.header}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String authHeader = httpServletRequest.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(tokenHead)) {
final String authToken = authHeader.substring(tokenHead.length()); // The part after "Bearer "
String useraccount = jwtUtil.getUserAccountFromToken(authToken);
logger.info("JwtAuthenticationTokenFilter[doFilterInternal] checking authentication " + useraccount);
if (useraccount != null && SecurityContextHolder.getContext().getAuthentication() == null) {//token校驗通過
UserDetails userDetails = this.userDetailsService.loadUserByUsername(useraccount);//根據account去資料庫中查詢user資料,足夠信任token的情況下,可以省略這一步
if (jwtUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
httpServletRequest));
logger.info("JwtAuthenticationTokenFilter[doFilterInternal] authenticated user " + useraccount + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
配置filter以及攔截url:
/**
* \* Created: liuhuichao
* \* Date: 2017/10/30
* \* Time: 上午11:01
* \* Description:spring security 的安全配置類
* \
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Autowired
private UserDetailsService userDetailsService;
@Value("${jwt.exceptUrl}")
private String exceptUrl;
/**
* 使用者名稱密碼認證方法
* @param authenticationManagerBuilder
* @throws Exception
*/
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
// 設定UserDetailsService
.userDetailsService(this.userDetailsService);
}
/**
* 裝載BCrypt密碼編碼器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter();
}
/**
* token請求授權
* @param httpSecurity
* @throws Exception
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 由於使用的是JWT,我們這裡不需要csrf
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()//未授權處理
// 基於token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// 對於獲取token的rest api要允許匿名訪問
.antMatchers(exceptUrl).permitAll()
// 除上面外的所有請求全部需要鑑權認證
.anyRequest().authenticated();
// 新增JWT filter
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); //將token驗證新增在密碼驗證前面
// 禁用快取
httpSecurity.headers().cacheControl();
}
}
處理異常:
/**
* jwt 未授權
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
JSONObject result=new JSONObject();
JSONObject header=new JSONObject();
if(authException instanceof BadCredentialsException){ /**身份認證未通過*/
header.put("errorcode","8002");
header.put("errorinfo","使用者名稱或密碼錯誤,請重新輸入!");
result.put("header",header);
}else{
header.put("errorcode","8001");
header.put("errorinfo","無效的token");
result.put("header",header);
}
response.getWriter().write(JSONObject.toJSONString(result));
}
}