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