Spring Boot 鑒權之—— JWT 鑒權
第一:什麽是JWT鑒權
1. JWT即JSON Web Tokens,是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準((RFC 7519),他可以用來安全的傳遞信息,因為傳遞的信息是經過加密算法加密過得。
2.JWT常用的加密算法有:HMAC算法或者是RSA的公私秘鑰對進行簽名,也可以使用公鑰/私鑰的非對稱算法
3.JWT的使用場景主要包括:
1) 認證授權,特別適用於分布式站點的單點登錄(SSO)場景,只要用戶開放的登錄入口登錄過一次系統,就會返回一個token,之後的請求都需要包含token。
2)交換信息,通過使用密鑰對來安全的傳送信息,可以知道發送者是誰、放置消息是否被篡改,一般被用來在身份提供者和服務提供者之間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,例如:設備信息,版本號等,該token也可直接被用於認證,也可被加密。
第二:JWT構成
JSON Web Tokens(JWT)有三部分構成,用英文句點分割(.) ,一般看起來例如:xxxxx.yyyyy.zzzzz
分為:
Header 頭信息
Payload 荷載信息,實際數據
Signature 由頭信息+荷載信息+密鑰 組合之後進行加密得到
1) Header 頭信息通常包含兩部分,type:代表token的類型,這裏使用的是JWT類型。 alg:代表使用的算法,例如HMAC SHA256或RSA.
{
"alg": "HS256",
"typ": "JWT"
} // 這會被經過base64Url編碼形成第一部分
2)Payload 一個token的第二部分是荷載信息,它包含一些聲明Claim(實體的描述,例:用戶信息和其他的一些元數據)
聲明分三類:
1)Reserved Claims,這是一套預定義的聲明,並不是必須的,這是一套易於使用、操作性強的聲明。包括:iss(issuer)、exp(expiration time)、sub(subject)、aud(audience)等
2)Plubic Claims,
3)Private Claims,交換信息的雙方自定義的聲明
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}//同樣經過Base64Url編碼後形成第二部分
3) signature 使用header中指定的算法將編碼後的header、編碼後的payload、一個secret進行加密
例如使用的是HMAC SHA256算法,大致流程類似於: HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
這個signature字段被用來確認JWT信息的發送者是誰,並保證信息沒有被修改
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
第三步:JWT認證流程
上圖是官方提供的一個認證流程圖 ,我們可以看到它的授權流程是:
1.客戶端通過post請求請求服務端登錄認證接口
2.服務端用秘密創建JWT
3.服務端將JWT返回瀏覽器
4.客戶端在授權報頭上發送JWT
5.服務端檢查JWT簽名從JWT獲取用戶信息
6.服務端向客戶端發送響應
通常我們所看到的認證流程,只能看到第一步和第六步,如果使用調試模式或者用抓包工具抓取就可以看到完整流程。
第四步:jwt使用
1)、 引入相關jar:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<!-- 使用lombok優雅的編碼 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
2)、jwt只要編碼
JwtUtil:jwt工具類
import org.springframework.util.StringUtils;
/**
* jwt工具類
* @author zyl
*
*/
public class JwtUtils {
private static final String AUTHORIZATION_HEADER_PREFIX = "Bearer ";
/**
* 獲取原始令牌
* remove ‘Bearer ‘ string
*
* @param authorizationHeader
* @return
*/
public static String getRawToken(String authorizationHeader) {
return authorizationHeader.substring(AUTHORIZATION_HEADER_PREFIX.length());
}
/**
* 獲取令牌頭
* @param rawToken
* @return
*/
public static String getTokenHeader(String rawToken) {
return AUTHORIZATION_HEADER_PREFIX + rawToken;
}
/**
* 驗證授權請求頭
* @param authorizationHeader
* @return
*/
public static boolean validate(String authorizationHeader) {
return StringUtils.hasText(authorizationHeader) && authorizationHeader.startsWith(AUTHORIZATION_HEADER_PREFIX);
}
/**
* 獲取授權頭前綴
* @return
*/
public static String getAuthorizationHeaderPrefix() {
return AUTHORIZATION_HEADER_PREFIX;
}
}
JwtAuthenticationFilter JWT認證過濾器
import com.example.demo.util.JwtUtils;
import io.jsonwebtoken.Jwts;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
/**
* JWT認證過濾器
* @author zyl
*
*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith(JwtUtils.getAuthorizationHeaderPrefix())) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authenticationToken = getUsernamePasswordAuthenticationToken(header);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getUsernamePasswordAuthenticationToken(String token) {
String user = Jwts.parser()
.setSigningKey("PrivateSecret")
.parseClaimsJws(token.replace(JwtUtils.getAuthorizationHeaderPrefix(), ""))
.getBody()
.getSubject();
if (null != user) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
}
JwtLoginFilter jwt登錄過濾器
import com.example.demo.domain.Employee;
import com.example.demo.util.JwtUtils;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
/**
* JWT 登錄過濾器
* @author zyl
*
*/
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JwtLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
Employee employee = new Employee();
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
employee.getUsername(),
employee.getPassword(),
new ArrayList<>()
)
);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
String token = Jwts.builder()
.setSubject(((User) authResult.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
.signWith(SignatureAlgorithm.HS512, "PrivateSecret")
.compact();
response.addHeader("Authorization", JwtUtils.getTokenHeader(token));
}
}
SecurityConfiguration config配置
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import com.jwt.server.filter.JwtAuthenticationFilter;
import com.jwt.server.filter.JwtLoginFilter;
/**
* 通過SpringSecurity的配置,將JWTLoginFilter,JWTAuthenticationFilter組合在一起
*
* @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) 在springboot1.5.8的時候該註解是可以用的 具體看源碼
* @author zyl
*
*/
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//自定義 默認
http.cors().and().csrf().disable().authorizeRequests() .antMatchers("/user/login","/login", "/oauth/authorize").permitAll()
.anyRequest().authenticated()
.and()
.requestMatchers().antMatchers("/user/login","/login","/oauth/authorize")
.and()
.addFilter(new JwtLoginFilter(authenticationManager()))//默認登錄過濾器
.addFilter(new JwtAuthenticationFilter(authenticationManager()));//自定義過濾器
}
}
UserInfo 認證用戶
import lombok.Data;
/**
* 認證用戶
* @author zyl
*
*/
@Data
public class UserInfo {
private String id;
private String username;
private String password;
public UserInfo() {
this.setId("testId");
this.setUsername("testUsername");
this.setPassword("testPassword");
}
}
UserDetailServiceImpl:核心認證用戶service類
import static java.util.Collections.emptyList;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.jwt.server.domain.UserInfo;
/**
*
* @author zyl
*
*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
UserInfo user = new UserInfo();
return new User(user.getUsername(), user.getPassword(), emptyList());
}
}
第五步:測試
請求登錄:localhost:8085/login
這裏測試自定義登錄的是請用post方式,因為默認源碼裏邊只支持post方式
自定義登錄:localhost:8085/user/login
源碼地址:https://github.com/GitHubZhangCom/spring-security-oauth-example/
註意:我在測試的時候發現springboot 2.0的版本不支持默認登錄的匿名登錄方式
成功測試,是在1.5.8的版本。
這是在2.0.4版本的時候默認登錄報的錯:
這裏因時間原因,暫不做處理,後期有時間我會同步更新源碼和博客。
Spring Boot 鑒權之—— JWT 鑒權