JJWT aims to be the easiest to use and understand library for creating and verifying JSON Web Tokens (JWTs) on the JVM.css
JJWT is a Java implementation based on the JWT, JWS, JWE, JWK and JWA RFC specifications.html
The library was created by Stormpath's CTO, Les Hazlewood and is now maintained by a community of contributors.html5
Stormpath is a complete authentication and user management API for developers.java
We've also added some convenience extensions that are not part of the specification, such as JWT compression and claim enforcement.react
Don't know what a JSON Web Token is? Read on. Otherwise, jump on down to the Installation section.nginx
JWT is a means of transmitting information between two parties in a compact, verifiable form.git
The bits of information encoded in the body of a JWT are called claims
. The expanded form of the JWT is in a JSON format, so each claim
is a key in the JSON object.github
JWTs can be cryptographically signed (making it a JWS) or encrypted (making it a JWE).web
This adds a powerful layer of verifiability to the user of JWTs. The receiver has a high degree of confidence that the JWT has not been tampered with by verifying the signature, for instance.算法
The compacted representation of a signed JWT is a string that has three parts, each separated by a .
:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.ipevRNuRP6HflG8cFKnmUPtypruRC4fb1DWtoLL62SY
Each section is base 64 encoded. The first section is the header, which at a minimum needs to specify the algorithm used to sign the JWT. The second section is the body. This section has all the claims of this JWT encoded in it. The final section is the signature. It's computed by passing a combination of the header and body through the algorithm specified in the header.
If you pass the first two sections through a base 64 decoder, you'll get the following (formatting added for clarity):
header
{ "alg": "HS256" }
body
{ "sub": "Joe" }
In this case, the information we have is that the HMAC using SHA-256 algorithm was used to sign the JWT. And, the body has a single claim, sub
with value Joe
.
There are a number of standard claims, called Registered Claims, in the specification and sub
(for subject) is one of them.
To compute the signature, you must know the secret that was used to sign it. In this case, it was the word secret
. You can see the signature creation is action here (Note: Trailing =
are lopped off the signature for the JWT).
Now you know (just about) all you need to know about JWTs.
Use your favorite Maven-compatible build tool to pull the dependency (and its transitive dependencies) from Maven Central:
Maven:
<dependency>
<groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency>
Gradle:
dependencies {
compile 'io.jsonwebtoken:jjwt:0.7.0' }
Note: JJWT depends on Jackson 2.x. If you're already using an older version of Jackson in your app, read this
Most complexity is hidden behind a convenient and readable builder-based fluent interface, great for relying on IDE auto-completion to write code quickly. Here's an example:
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.impl.crypto.MacProvider; import java.security.Key; // We need a signing key, so we'll create one just for this example. Usually // the key would be read from your application configuration instead. Key key = MacProvider.generateKey(); String compactJws = Jwts.builder() .setSubject("Joe") .signWith(SignatureAlgorithm.HS512, key) .compact();
How easy was that!?
In this case, we are building a JWT that will have the registered claim sub
(subject) set to Joe
. We are signing the JWT using the HMAC using SHA-512 algorithm. finally, we are compacting it into its String
form.
The resultant String
looks like this:
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJKb2UifQ.yiV1GWDrQyCeoOswYTf_xvlgsnaVVYJM0mU6rkmRBf2T1MBl3Xh2kZii0Q9BdX5-G0j25Qv2WF4lA6jPl5GKuA
Now let's verify the JWT (you should always discard JWTs that don't match an expected signature):
assert Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws).getBody().getSubject().equals("Joe");
There are two things going on here. The key
from before is being used to validate the signature of the JWT. If it fails to verify the JWT, a SignatureException
is thrown. Assuming the JWT is validated, we parse out the claims and assert that that subject is set to Joe
.
You have to love code one-liners that pack a punch!
But what if signature validation failed? You can catch SignatureException
and react accordingly:
try {
Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws); //OK, we can trust this JWT } catch (SignatureException e) { //don't trust the JWT! }
Creating and parsing plaintext compact JWTs
Creating, parsing and verifying digitally signed compact JWTs (aka JWSs) with all standard JWS algorithms:
Body compression. If the JWT body is large, you can use a CompressionCodec
to compress it. Best of all, the JJWT library will automtically decompress and parse the JWT without additional coding.
String compactJws = Jwts.builder() .setSubject("Joe") .compressWith(CompressionCodecs.DEFLATE) .signWith(SignatureAlgorithm.HS512, key) .compact();
If you examine the header section of the compactJws
, it decodes to this:
{ "alg": "HS512", "zip": "DEF" }
JJWT automatically detects that compression was used by examining the header and will automatically decompress when parsing. No extra coding is needed on your part for decompression.
Require Claims. When parsing, you can specify that certain claims must be present and set to a certain value.
try {
Jws<Claims> claims = Jwts.parser() .requireSubject("Joe") .require("hasMotorcycle", true) .setSigningKey(key) .parseClaimsJws(compactJws); } catch (MissingClaimException e) { // we get here if the required claim is not present } catch (IncorrectClaimException e) { // we get here if the required claim has the wrong value }
These feature sets will be implemented in a future release. Community contributions are welcome!
JJWT depends on Jackson 2.8.x (or later). If you are already using a Jackson version in your own application less than 2.x, for example 1.9.x, you will likely see runtime errors. To avoid this, you should change your project build configuration to explicitly point to a 2.x version of Jackson. For example:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.8.2</version> </dependency>
Maintained by Stormpath
http://www.cnblogs.com/softidea/p/6204673.html
https://github.com/jwtk/jjwt
The following Claim Names are registered in the IANA "JSON Web Token
Claims" registry established by Section 10.1. None of the claims
defined below are intended to be mandatory to use or implement in all
cases, but rather they provide a starting point for a set of useful,
interoperable claims. Applications using JWTs should define which
specific claims they use and when they are required or optional. All
the names are short because a core goal of JWTs is for the
representation to be compact.
The "iss" (issuer) claim identifies the principal that issued the JWT. The processing of this claim is generally application specific. The "iss" value is a case-sensitive string containing a StringOrURI value. Use of this claim is OPTIONAL.
The "sub" (subject) claim identifies the principal that is the subject of the JWT. The claims in a JWT are normally statements about the subject. The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. The processing of this claim is generally application specific. The "sub" value is a case-sensitive string containing a StringOrURI value. Use of this claim is OPTIONAL.
The "aud" (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the "aud" claim when this claim is present, then the JWT MUST be rejected. In the general case, the "aud" value is an array of case- sensitive strings, each containing a StringOrURI value. In the special case when the JWT has one audience, the "aud" value MAY be a single case-sensitive string containing a StringOrURI value. The interpretation of audience values is generally application specific. Use of this claim is OPTIONAL.
The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. The processing of the "exp" claim requires that the current date/time MUST be before the expiration date/time listed in the "exp" claim.
https://tools.ietf.org/html/rfc7519#section-4.1
原文 https://juejin.im/post/58c29e0b1b69e6006bce02f4
一般狀況下,把API直接暴露出去是風險很大的,不說別的,直接被機器攻擊就喝一壺的。那麼通常來講,對API要劃分出必定的權限級別,而後作一個用戶的鑑權,依據鑑權結果給予用戶開放對應的API。目前,比較主流的方案有幾種:
第一種就不介紹了,因爲依賴Session來維護狀態,也不太適合移動時代,新的項目就不要採用了。第二種OAuth的方案和JWT都是基於Token的,但OAuth其實對於不作開放平臺的公司有些過於複雜。咱們主要介紹第三種:JWT。
JWT是 Json Web Token
的縮寫。它是基於 RFC 7519 標準定義的一種能夠安全傳輸的 小巧 和 自包含 的JSON對象。因爲數據是使用數字簽名的,因此是可信任的和安全的。JWT可使用HMAC算法對secret進行加密或者使用RSA的公鑰私鑰對來進行簽名。
下面是一個JWT的工做流程圖。模擬一下實際的流程是這樣的(假設受保護的API在 /protected
中)
/protected
中的API時,在請求的header中加入 Authorization: Bearer xxxx(token)
。此處注意token以前有一個7字符長度的 Bearer
JWT工做流程圖
爲了更好的理解這個token是什麼,咱們先來看一個token生成後的樣子,下面那坨亂糟糟的就是了。
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ3YW5nIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTM5MywiZXhwIjoxNDg5Njg0NzgxfQ.RC-BYCe_UZ2URtWddUpWXIp4NMsoeq2O6UF-8tVplqXY1-CI9u1-a-9DAAJGfNWkHE81mpnR3gXzfrBAB3WUAg
但仔細看到的話仍是能夠看到這個token分紅了三部分,每部分用 .
分隔,每段都是用 Base64編碼的。若是咱們用一個Base64的解碼器的話 ( https://www.base64decode.org/ ),能夠看到第一部分 eyJhbGciOiJIUzUxMiJ9
被解析成了:
{
"alg":"HS512" }
這是告訴咱們HMAC採用HS512算法對JWT進行的簽名。
第二部分 eyJzdWIiOiJ3YW5nIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTM5MywiZXhwIjoxNDg5Njg0NzgxfQ
被解碼以後是
{
"sub":"wang", "created":1489079981393, "exp":1489684781 }
這段告訴咱們這個Token中含有的數據聲明(Claim),這個例子裏面有三個聲明: sub
, created
和 exp
。在咱們這個例子中,分別表明着用戶名、建立時間和過時時間,固然你能夠把任意數據聲明在這裏。
看到這裏,你可能會想這是個什麼鬼token,全部信息都透明啊,安全怎麼保障?別急,咱們看看token的第三段 RC-BYCe_UZ2URtWddUpWXIp4NMsoeq2O6UF-8tVplqXY1-CI9u1-a-9DAAJGfNWkHE81mpnR3gXzfrBAB3WUAg
。一樣使用Base64解碼以後,咦,這是什麼東東
D X DmYTeȧLUZcPZ0$gZAY_7wY@
最後一段實際上是簽名,這個簽名必須知道祕鑰才能計算。這個也是JWT的安全保障。這裏提一點注意事項,因爲數據聲明(Claim)是公開的,千萬不要把密碼等敏感字段放進去,不然就等因而公開給別人了。
也就是說JWT是由三段組成的,按官方的叫法分別是header(頭)、payload(負載)和signature(簽名):
header.payload.signature
頭中的數據一般包含兩部分:一個是咱們剛剛看到的 alg
,這個詞是 algorithm
的縮寫,就是指明算法。另外一個能夠添加的字段是token的類型(按RFC 7519實現的token機制不僅JWT一種),但若是咱們採用的是JWT的話,指定這個就多餘了。
{
"alg": "HS512", "typ": "JWT" }
payload中能夠放置三類數據:系統保留的、公共的和私有的:
簽名的過程是這樣的:採用header中聲明的算法,接受三個參數:base64編碼的header、base64編碼的payload和祕鑰(secret)進行運算。簽名這一部分若是你願意的話,能夠採用RSASHA256的方式進行公鑰、私鑰對的方式進行,若是安全性要求的高的話。
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
爲了簡化咱們的工做,這裏引入一個比較成熟的JWT類庫,叫 jjwt
( https://github.com/jwtk/jjwt )。這個類庫能夠用於Java和Android的JWT token的生成和驗證。
JWT的生成可使用下面這樣的代碼完成:
String generateToken(Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, secret) //採用什麼算法是能夠本身選擇的,不必定非要採用HS512 .compact(); }
數據聲明(Claim)其實就是一個Map,好比咱們想放入用戶名,能夠簡單的建立一個Map而後put進去就能夠了。
Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, username());
解析也很簡單,利用 jjwt
提供的parser傳入祕鑰,而後就能夠解析token了。
Claims getClaimsFromToken(String token) {
Claims claims; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { claims = null; } return claims; }
JWT自己沒啥難度,但安全總體是一個比較複雜的事情,JWT只不過提供了一種基於token的請求驗證機制。但咱們的用戶權限,對於API的權限劃分、資源的權限劃分,用戶的驗證等等都不是JWT負責的。也就是說,請求驗證後,你是否有權限看對應的內容是由你的用戶角色決定的。因此咱們這裏要利用Spring的一個子項目Spring Security來簡化咱們的工做。
Spring Security是一個基於Spring的通用安全框架,裏面內容太多了,本文的主要目的也不是展開講這個框架,而是如何利用Spring Security和JWT一塊兒來完成API保護。因此關於Spring Secruity的基礎內容或展開內容,請自行去官網學習( http://projects.spring.io/spring-security/ )。
若是你的系統有用戶的概念的話,通常來講,你應該有一個用戶表,最簡單的用戶表,應該有三列:Id,Username和Password,相似下表這種
ID | USERNAME | PASSWORD |
---|---|---|
10 | wang | abcdefg |
並且不是全部用戶都是一種角色,好比網站管理員、供應商、財務等等,這些角色和網站的直接用戶須要的權限多是不同的。那麼咱們就須要一個角色表:
ID | ROLE |
---|---|
10 | USER |
20 | ADMIN |
固然咱們還須要一個能夠將用戶和角色關聯起來創建映射關係的表。
USER_ID | ROLE_ID |
---|---|
10 | 10 |
20 | 20 |
這是典型的一個關係型數據庫的用戶角色的設計,因爲咱們要使用的MongoDB是一個文檔型數據庫,因此讓咱們從新審視一下這個結構。
這個數據結構的優勢在於它避免了數據的冗餘,每一個表負責本身的數據,經過關聯表進行關係的描述,同時也保證的數據的完整性:好比當你修改角色名稱後,沒有髒數據的產生。
可是這種事情在用戶權限這個領域發生的頻率到底有多少呢?有多少人天天不停的改的角色名稱?固然若是你的業務場景確實是須要保證數據完整性,你仍是應該使用關係型數據庫。但若是沒有高頻的對於角色表的改動,其實咱們是不須要這樣的一個設計的。在MongoDB中咱們能夠將其簡化爲
{
_id: <id_generated>
username: 'user', password: 'pass', roles: ['USER', 'ADMIN'] }
基於以上考慮,咱們重構一下 User
類,
@Data
public class User { @Id private String id; @Indexed(unique=true, direction= IndexDirection.DESCENDING, dropDups=true) private String username; private String password; private String email; private Date lastPasswordResetDate; private List<String> roles; }
固然你可能發現這個類有點怪,只有一些field,這個簡化的能力是一個叫 lombok
類庫提供的 ,這個不少開發過Android的童鞋應該熟悉,是用來簡化POJO的建立的一個類庫。簡單說一下,採用 lombok
提供的 @Data
修飾符後能夠簡寫成,原來的一坨getter和setter以及constructor等都不須要寫了。相似的 Todo
能夠改寫成:
@Data
public class Todo { @Id private String id; private String desc; private boolean completed; private User user; }
增長這個類庫只需在 build.gradle
中增長下面這行
dependencies {
// 省略其它依賴 compile("org.projectlombok:lombok:${lombokVersion}") }
要在Spring Boot中引入Spring Security很是簡單,修改 build.gradle
,增長一個引用 org.springframework.boot:spring-boot-starter-security
:
dependencies {
compile("org.springframework.boot:spring-boot-starter-data-rest") compile("org.springframework.boot:spring-boot-starter-data-mongodb") compile("org.springframework.boot:spring-boot-starter-security") compile("io.jsonwebtoken:jjwt:${jjwtVersion}") compile("org.projectlombok:lombok:${lombokVersion}") testCompile("org.springframework.boot:spring-boot-starter-test") }
你可能發現了,咱們不僅增長了對Spring Security的編譯依賴,還增長 jjwt
的依賴。
Spring Security須要咱們實現幾個東西,第一個是UserDetails:這個接口中規定了用戶的幾個必需要有的方法,因此咱們建立一個JwtUser類來實現這個接口。爲何不直接使用User類?由於這個UserDetails徹底是爲了安全服務的,它和咱們的領域類可能有部分屬性重疊,但不少的接口實際上是安全定製的,因此最好新建一個類:
public class JwtUser implements UserDetails { private final String id; private final String username; private final String password; private final String email; private final Collection<? extends GrantedAuthority> authorities; private final Date lastPasswordResetDate; public JwtUser( String id, String username, String password, String email, Collection<? extends GrantedAuthority> authorities, Date lastPasswordResetDate) { this.id = id; this.username = username; this.password = password; this.email = email; this.authorities = authorities; this.lastPasswordResetDate = lastPasswordResetDate; } //返回分配給用戶的角色列表 @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @JsonIgnore public String getId() { return id; } @JsonIgnore @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } // 帳戶是否未過時 @JsonIgnore @Override public boolean isAccountNonExpired() { return true; } // 帳戶是否未鎖定 @JsonIgnore @Override public boolean isAccountNonLocked() { return true; } // 密碼是否未過時 @JsonIgnore @Override public boolean isCredentialsNonExpired() { return true; } // 帳戶是否激活 @JsonIgnore @Override public boolean isEnabled() { return true; } // 這個是自定義的,返回上次密碼重置日期 @JsonIgnore public Date getLastPasswordResetDate() { return lastPasswordResetDate; } }
這個接口中規定的不少方法咱們都簡單粗暴的設成直接返回某個值了,這是爲了簡單起見,你在實際開發環境中仍是要根據具體業務調整。固然因爲兩個類仍是有必定關係的,爲了寫起來簡單,咱們寫一個工廠類來由領域對象建立 JwtUser
,這個工廠就叫 JwtUserFactory
吧:
public final class JwtUserFactory { private JwtUserFactory() { } public static JwtUser create(User user) { return new JwtUser( user.getId(), user.getUsername(), user.getPassword(), user.getEmail(), mapToGrantedAuthorities(user.getRoles()), user.getLastPasswordResetDate() ); } private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) { return authorities.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); } }
第二個要實現的是 UserDetailsService
,這個接口只定義了一個方法 loadUserByUsername
,顧名思義,就是提供一種從用戶名能夠查到用戶並返回的方法。注意,不必定是數據庫哦,文本文件、xml文件等等均可能成爲數據源,這也是爲何Spring提供這樣一個接口的緣由:保證你能夠採用靈活的數據源。接下來咱們創建一個 JwtUserDetailsServiceImpl
來實現這個接口。
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if (user == null) { throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username)); } else { return JwtUserFactory.create(user); } } }
爲了讓Spring能夠知道咱們想怎樣控制安全性,咱們還須要創建一個安全配置類 WebSecurityConfig
:
@Configuration
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ // Spring會自動尋找一樣類型的具體類注入,這裏就是JwtUserDetailsServiceImpl了 @Autowired private UserDetailsService userDetailsService; @Autowired public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder // 設置UserDetailsService .userDetailsService(this.userDetailsService) // 使用BCrypt進行密碼的hash .passwordEncoder(passwordEncoder()); } // 裝載BCrypt密碼編碼器 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity // 因爲使用的是JWT,咱們這裏不須要csrf .csrf().disable() // 基於token,因此不須要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() //.antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 容許對於網站靜態資源的無受權訪問 .antMatchers( HttpMethod.GET, "/", "/*.html", "/favicon.ico", "/**/*.html", "/**/*.css", "/**/*.js" ).permitAll() // 對於獲取token的rest api要容許匿名訪問 .antMatchers("/auth/**").permitAll() // 除上面外的全部請求所有須要鑑權認證 .anyRequest().authenticated(); // 禁用緩存 httpSecurity.headers().cacheControl(); } }
接下來咱們要規定一下哪些資源須要什麼樣的角色能夠訪問了,在 UserController
加一個修飾符 @PreAuthorize("hasRole('ADMIN')")
表示這個資源只能被擁有 ADMIN
角色的用戶訪問。
/**
* 在 @PreAuthorize 中咱們能夠利用內建的 SPEL 表達式:好比 'hasRole()' 來決定哪些用戶有權訪問。
* 需注意的一點是 hasRole 表達式認爲每一個角色名字前都有一個前綴 'ROLE_'。因此這裏的 'ADMIN' 其實在
* 數據庫中存儲的是 'ROLE_ADMIN' 。這個 @PreAuthorize 能夠修飾Controller也可修飾Controller中的方法。
**/
@RestController @RequestMapping("/users") @PreAuthorize("hasRole('ADMIN')") public class UserController { @Autowired private UserRepository repository; @RequestMapping(method = RequestMethod.GET) public List<User> getUsers() { return repository.findAll(); } // 略去其它部分 }
相似的咱們給 TodoController
加上 @PreAuthorize("hasRole('USER')")
,標明這個資源只能被擁有 USER
角色的用戶訪問:
@RestController
@RequestMapping("/todos") @PreAuthorize("hasRole('USER')") public class TodoController { // 略去 }
如今應該Spring Security能夠工做了,但爲了能夠更清晰的看到工做日誌,咱們但願配置一下,在和 src
同級創建一個config文件夾,在這個文件夾下面新建一個 application.yml
。
# Server configuration
server: port: 8090 contextPath: # Spring configuration spring: jackson: serialization: INDENT_OUTPUT: true data.mongodb: host: localhost port: 27017 database: springboot # Logging configuration logging: level: org.springframework: data: DEBUG security: DEBUG
咱們除了配置了logging的一些東東外,也順手設置了數據庫和http服務的一些配置項,如今咱們的服務器會在8090端口監聽,而spring data和security的日誌在debug模式下會輸出到console。
如今啓動服務後,訪問 http://localhost:8090
你能夠看到根目錄仍是正常顯示的
根目錄仍是正常能夠訪問的
但咱們試一下 http://localhost:8090/users
,觀察一下console,咱們會看到以下的輸出,告訴因爲用戶未鑑權,咱們訪問被拒絕了。
2017-03-10 15:51:53.351 DEBUG 57599 --- [nio-8090-exec-4] o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is anonymous); redirecting to authentication entry point org.springframework.security.access.AccessDeniedException: Access is denied at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-4.2.1.RELEASE.jar:4.2.1.RELEASE]
到如今,咱們仍是讓JWT和Spring Security各自爲戰,並無集成起來。要想要JWT在Spring中工做,咱們應該新建一個filter,並把它配置在 WebSecurityConfig
中。
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Value("${jwt.header}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader(this.tokenHeader); if (authHeader != null && authHeader.startsWith(tokenHead)) { final String authToken = authHeader.substring(tokenHead.length()); // The part after "Bearer " String username = jwtTokenUtil.getUsernameFromToken(authToken); logger.info("checking authentication " + username); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails( request)); logger.info("authenticated user " + username + ", setting security context"); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); } }
事實上若是咱們足夠相信token中的數據,也就是咱們足夠相信簽名token的secret的機制足夠好,這種狀況下,咱們能夠不用再查詢數據庫,而直接採用token中的數據。本例中,咱們仍是經過Spring Security的 @UserDetailsService
進行了數據查詢,但簡單驗證的話,你能夠採用直接驗證token是否合法來避免昂貴的數據查詢。
接下來,咱們會在 WebSecurityConfig
中注入這個filter,而且配置到 HttpSecurity
中:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ // 省略其它部分 @Bean public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception { return new JwtAuthenticationTokenFilter(); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { // 省略以前寫的規則部分,具體看前面的代碼 // 添加JWT filter httpSecurity .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); } }
到如今,咱們整個API其實已經在安全的保護下了,但咱們遇到一個問題:全部的API都安全了,但咱們尚未用戶啊,因此全部API都無法訪問。所以要提供一個註冊、登陸的API,這個API應該是能夠匿名訪問的。給它規劃的路徑呢,咱們前面其實在 WebSecurityConfig
中已經給出了,就是 /auth
。
首先須要一個AuthService,規定一下必選動做:
public interface AuthService { User register(User userToAdd); String login(String username, String password); String refresh(String oldToken); }
而後,實現這些必選動做,其實很是簡單:
ROLE_USER
@Service
public class AuthServiceImpl implements AuthService { private AuthenticationManager authenticationManager; private UserDetailsService userDetailsService; private JwtTokenUtil jwtTokenUtil; private UserRepository userRepository; @Value("${jwt.tokenHead}") private String tokenHead; @Autowired public AuthServiceImpl( AuthenticationManager authenticationManager, UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, UserRepository userRepository) { this.authenticationManager = authenticationManager; this.userDetailsService = userDetailsService; this.jwtTokenUtil = jwtTokenUtil; this.userRepository = userRepository; } @Override public User register(User userToAdd) { final String username = userToAdd.getUsername(); if(userRepository.findByUsername(username)!=null) { return null; } BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); final String rawPassword = userToAdd.getPassword(); userToAdd.setPassword(encoder.encode(rawPassword)); userToAdd.setLastPasswordResetDate(new Date()); userToAdd.setRoles(asList("ROLE_USER")); return userRepository.insert(userToAdd); } @Override public String login(String username, String password) { UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password); final Authentication authentication = authenticationManager.authenticate(upToken); SecurityContextHolder.getContext().setAuthentication(authentication); final UserDetails userDetails = userDetailsService.loadUserByUsername(username); final String token = jwtTokenUtil.generateToken(userDetails); return token; } @Override public String refresh(String oldToken) { final String token = oldToken.substring(tokenHead.length()); String username = jwtTokenUtil.getUsernameFromToken(token); JwtUser user = (JwtUser) userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.canTokenBeRefreshed(token, user.getLastPasswordResetDate())){ return jwtTokenUtil.refreshToken(token); } return null; } }
而後創建AuthController就好,這個AuthController中咱們在其中使用了表達式綁定,好比 @Value("${jwt.header}")
中的 jwt.header
實際上是定義在 applicaiton.yml
中的
# JWT
jwt: header: Authorization secret: mySecret expiration: 604800 tokenHead: "Bearer " route: authentication: path: auth refresh: refresh register: "auth/register"
一樣的 @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
中的 jwt.route.authentication.path
也是定義在上面的
@RestController
public class AuthController { @Value("${jwt.header}") private String tokenHeader; @Autowired private AuthService authService; @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST) public ResponseEntity<?> createAuthenticationToken( @RequestBody JwtAuthenticationRequest authenticationRequest) throws AuthenticationException{ final String token = authService.login(authenticationRequest.getUsername(), authenticationRequest.getPassword()); // Return the token return ResponseEntity.ok(new JwtAuthenticationResponse(token)); } @RequestMapping(value = "${jwt.route.authentication.refresh}", method = RequestMethod.GET) public ResponseEntity<?> refreshAndGetAuthenticationToken( HttpServletRequest request) throws AuthenticationException{ String token = request.getHeader(tokenHeader); String refreshedToken = authService.refresh(token); if(refreshedToken == null) { return ResponseEntity.badRequest().body(null); } else { return ResponseEntity.ok(new JwtAuthenticationResponse(refreshedToken)); } } @RequestMapping(value = "${jwt.route.authentication.register}", method = RequestMethod.POST) public User register(@RequestBody User addedUser) throws AuthenticationException{ return authService.register(addedUser); } }
接下來,咱們就能夠看看咱們的成果了,首先註冊一個用戶 peng2
,很完美的註冊成功了
註冊用戶
而後在 /auth
中取得token,也很成功
取得token
不使用token時,訪問 /users
的結果,不出意料的失敗,提示未受權。
不使用token訪問users列表
使用token時,訪問 /users
的結果,雖然還是失敗,但此次提示訪問被拒絕,意思就是雖然你已經獲得了受權,但因爲你的會員級別還只是普卡會員,因此你的請求被拒絕。
image_1bas22va52vk1rj445fhm87k72a.png-156.9kB
接下來咱們訪問 /users/?username=peng2
,居然能夠訪問啊
訪問本身的信息是容許的
這是因爲咱們爲這個方法定義的權限就是:擁有ADMIN角色或者是當前用戶自己。Spring Security真是很方便,很強大。
@PostAuthorize("returnObject.username == principal.username or hasRole('ROLE_ADMIN')") @RequestMapping(value = "/",method = RequestMethod.GET) public User getUserByUsername(@RequestParam(value="username") String username) { return repository.findByUsername(username); }
本章代碼: https://github.com/wpcfan/spring-boot-tut/tree/chap04
http://www.tuicool.com/articles/IVzuqaj
http://spring4all.com/question/93
https://tools.ietf.org/html/rfc7519#section-4.1
深刻淺出JWT(JSON Web Token )
JSON Web Token(JWT)是一個開放式標準(RFC 7519),它定義了一種緊湊(Compact)且自包含(Self-contained)的方式,用於在各方之間以JSON對象安全傳輸信息。 這些信息能夠經過數字簽名進行驗證和信任。 可使用祕密(使用HMAC算法)或使用RSA的公鑰/私鑰對對JWT進行簽名。
雖然JWT能夠加密以提供各方之間的保密性,但咱們將重點關注已簽名的令牌。 簽名的令牌能夠驗證其中包含的索賠的完整性,而加密令牌隱藏來自其餘方的索賠。 當令牌使用公鑰/私鑰對進行簽名時,簽名還證實只有持有私鑰的方是簽名方。
咱們來進一步解釋一些概念:
在緊湊的形式中,JWT包含三個由點(.)分隔的部分,它們分別是:
JWT結構一般以下所示:
xxxxx.yyyyy.zzzzz
下面咱們分別來介紹這三個部分:
Header一般由兩部分組成:令牌的類型,即JWT。和經常使用的散列算法,如HMAC SHA256或RSA。
例如:
{
"alg": "HS256", "typ": "JWT" }
Header部分的JSON被Base64Url編碼,造成JWT的第一部分。
這裏放聲明內容,能夠說就是存放溝通信息的地方,在定義上有3種聲明(Claims):
Registered claims(註冊聲明):
這些是一組預先定義的聲明,它們不是強制性的,但推薦使用,以提供一組有用的,可互操做的聲明。 其中一些是:iss
(發行者),exp
(到期時間),sub
(主題),aud
(受衆)等。#Registered Claim Names#
Private claims(私有聲明):
這些是爲了贊成使用它們可是既沒有登記,也沒有公開聲明的各方之間共享信息,而建立的定製聲明。
Playload示例以下:
{
"sub": "1234567890", "name": "John Doe", "admin": true }
Playload部分的JSON被Base64Url編碼,造成JWT的第二部分。
請注意,對於已簽名的令牌,此信息儘管受到篡改保護,但任何人均可以閱讀。 除非加密,不然不要將祕密信息放在JWT的有效內容或標題元素中。這也是不少文章爭論jwt安全性緣由,不要用 JWT 取代 Server-side 的 Session狀態機制。詳情請閱讀這篇文章:Stop Using Jwt For Sessions.
第三部分signature用來驗證發送請求者身份,由前兩部分加密造成。
要建立簽名部分,您必須採用編碼標頭,編碼有效載荷,祕鑰,標頭中指定的算法並簽名。
例如,若是你想使用HMAC SHA256算法,簽名將按照如下方式建立:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
JWT輸出的是三個由點分隔的Base64-URL字符串,能夠在HTML和HTTP環境中輕鬆傳遞,而與基於XML的標準(如SAML)相比,它更加緊湊。
如下JWT示例,它具備先前的標頭和有效負載編碼,而且使用祕鑰進行簽名。
咱們可使用jwt.io調試器來解碼,驗證和生成JWT:
在身份驗證中,當用戶使用他們的憑證成功登陸時,JSON Web Token將被返回而且必須保存在本地(一般在本地存儲中,但也可使用Cookie),而不是在傳統方法中建立會話 服務器並返回一個cookie。
關於存儲令牌(Token)的方式,必須考慮安全因素。
參考: #Where to Store Tokens#
不管什麼時候用戶想要訪問受保護的路由或資源,用戶代理都應使用承載方案發送JWT,一般在請求頭中的Authorization
字段,使用Bearer
schema:
Authorization: Bearer <token>
這是一種無狀態身份驗證機制,由於用戶狀態永遠不會保存在服務器內存中。 服務器受保護的路由將在受權頭中檢查有效的JWT,若是存在,則容許用戶訪問受保護的資源。 因爲JWT是獨立的,全部必要的信息都在那裏,減小了屢次查詢數據庫的需求。
這使得咱們能夠徹底依賴無狀態的數據API,甚至向下遊服務提出請求。 不管哪些域正在爲API提供服務並不重要,所以不會出現跨域資源共享(CORS)的問題,由於它不使用Cookie。
請注意,使用已簽名的令牌,令牌中包含的全部信息都會暴露給用戶或其餘方,即便他們沒法更改它。 在JWT中,不該該在Playload裏面加入任何敏感的數據,好比像密碼這樣的內容。若是將用戶的密碼放在了JWT中,那麼懷有惡意的第三方經過Base64解碼就能很快地知道你的密碼了。
Base64編碼方式是可逆的,也就是透過編碼後發放的Token內容是能夠被解析的。通常而言,咱們都不建議在有效載荷內放敏感訊息,好比使用者的密碼。
JWT其中的一個組成內容爲Signature,能夠防止經過Base64可逆方法回推有效載荷內容並將其修改。由於Signature是經由Header跟Payload一塊兒Base64組成的。
是的,Cookie丟失,就表示身份就能夠被僞造。故官方建議的使用方式是存放在LocalStorage中,並放在請求頭中發送。
JWT Token一般長度不會過小,特別是Stateless JWT Token,把全部的數據都編在Token裏,很快的就會超過Cookie的大小(4K)或者是URL長度限制。
exp
時效不要設定太長。Only Http
預防XSS攻擊。jti
(JWT ID),exp
(有效時間) Claim。[1] Stop using JWT for sessions:
http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/
[3] Use JWT The Right Way!:
https://stormpath.com/blog/jwt-the-right-way
[2] JSON Web Token 維基百科:
https://en.wikipedia.org/wiki/JSON_Web_Token
https://www.cnblogs.com/mantoudev/p/8994341.html