第一:什么是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使用
源碼地址:
github: https://github.com/GitHubZhangCom/spring-security-oauth-example/
碼雲:https://gitee.com/region/spring-security-oauth-example/tree/master/spring-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
import java.io.IOException; import java.util.ArrayList; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; 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 com.jwt.server.util.JwtUtils; import io.jsonwebtoken.Jwts; /** * 自定義JWT認證過濾器 * 該類繼承自BasicAuthenticationFilter,在doFilterInternal方法中, * 從http頭的Authorization 項讀取token數據,然后用Jwts包提供的方法校驗token的合法性。 * 如果校驗通過,就認為這是一個取得授權的合法請求 * @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
import java.io.IOException; import java.util.ArrayList; import java.util.Date; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; 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 com.fasterxml.jackson.databind.ObjectMapper; import com.jwt.server.domain.UserInfo; import com.jwt.server.util.JwtUtils; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; /** * 驗證用戶名密碼正確后,生成一個token,並將token返回給客戶端 * 該類繼承自UsernamePasswordAuthenticationFilter,重寫了其中的2個方法 attemptAuthentication * :接收並解析用戶憑證。 successfulAuthentication :用戶成功登錄后,這個方法會被調用,我們在這個方法里生成token。 * */ 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 { try { UserInfo user = new ObjectMapper().readValue(request.getInputStream(), UserInfo.class); return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>())); } catch (IOException e) { throw new RuntimeException(e); } } // 用戶成功登錄后,這個方法會被調用,我們在這個方法里生成token @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

