摘要:用spring-boot開發RESTful API非常的方便,在生產環境中,對發布的API增加授權保護是非常必要的。現在我們來看如何利用JWT技術為API增加授權保護,保證只有獲得授權的用戶才能夠訪問API。
一:開發一個簡單的API
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter</artifactId>
-
</dependency>
-
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter-test</artifactId>
-
<scope>test</scope>
-
</dependency>
-
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter-web</artifactId>
-
</dependency>
-
-
<!-- spring-data-jpa -->
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter-data-jpa</artifactId>
-
</dependency>
-
-
<!-- mysql -->
-
<dependency>
-
<groupId>mysql</groupId>
-
<artifactId>mysql-connector-java</artifactId>
-
<version>5.1.30</version>
-
</dependency>
-
-
<!-- spring-security 和 jwt -->
-
<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>
新建一個UserController.java文件,在里面在中增加一個hello方法:
-
-
-
public String hello(){
-
return "hello";
-
}
這樣一個簡單的RESTful API就開發好了。
現在我們運行一下程序看看效果,執行JwtauthApplication.java類中的main方法:
等待程序啟動完成后,可以簡單的通過curl工具進行API的調用,如下圖:
至此,我們的接口就開發完成了。但是這個接口沒有任何授權防護,任何人都可以訪問,這樣是不安全的,下面我們開始加入授權機制。
二:增加用戶注冊功能
-
package boss.portal.entity;
-
-
import javax.persistence.*;
-
-
/**
-
* @author zhaoxinguo on 2017/9/13.
-
*/
-
-
-
public class User {
-
-
-
-
private long id;
-
private String username;
-
private String password;
-
-
public long getId() {
-
return id;
-
}
-
-
public String getUsername() {
-
return username;
-
}
-
-
public void setUsername(String username) {
-
this.username = username;
-
}
-
-
public String getPassword() {
-
return password;
-
}
-
-
public void setPassword(String password) {
-
this.password = password;
-
}
-
}
-
package boss.portal.repository;
-
-
import boss.portal.entity.User;
-
import org.springframework.data.jpa.repository.JpaRepository;
-
-
/**
-
* @author zhaoxinguo on 2017/9/13.
-
*/
-
public interface UserRepository extends JpaRepository<User, Long> {
-
-
User findByUsername(String username);
-
-
}
-
/**
-
* 該方法是注冊用戶的方法,默認放開訪問控制
-
* @param user
-
*/
-
-
public void signUp(@RequestBody User user) {
-
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
-
applicationUserRepository.save(user);
-
}
這個方法定義了用戶注冊接口,並且指定了url地址是/users/signup。由於類上加了注解 @RequestMapping(“/users”),類中的所有方法的url地址都會有/users前綴,所以在方法上只需指定/signup子路徑即可。
密碼采用了BCryptPasswordEncoder進行加密,我們在Application中增加BCryptPasswordEncoder實例的定義。
-
package boss.portal;
-
-
import org.springframework.boot.SpringApplication;
-
import org.springframework.boot.autoconfigure.SpringBootApplication;
-
import org.springframework.context.annotation.Bean;
-
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
-
-
-
public class JwtauthApplication {
-
-
-
public BCryptPasswordEncoder bCryptPasswordEncoder() {
-
return new BCryptPasswordEncoder();
-
}
-
-
public static void main(String[] args) {
-
SpringApplication.run(JwtauthApplication.class, args);
-
}
-
}
三:增加JWT認證功能
用戶填入用戶名密碼后,與數據庫里存儲的用戶信息進行比對,如果通過,則認證成功。傳統的方法是在認證通過后,創建sesstion,並給客戶端返回cookie。現在我們采用JWT來處理用戶名密碼的認證。區別在於,認證通過后,服務器生成一個token,將token返回給客戶端,客戶端以后的所有請求都需要在http頭中指定該token。服務器接收的請求后,會對token的合法性進行驗證。驗證的內容包括:
-
內容是一個正確的JWT格式
-
檢查簽名
-
檢查claims
-
檢查權限
處理登錄
創建一個類JWTLoginFilter,核心功能是在驗證用戶名密碼正確后,生成一個token,並將token返回給客戶端:
-
package boss.portal.web.filter;
-
-
import boss.portal.entity.User;
-
import com.fasterxml.jackson.databind.ObjectMapper;
-
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.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;
-
-
/**
-
* 驗證用戶名密碼正確后,生成一個token,並將token返回給客戶端
-
* 該類繼承自UsernamePasswordAuthenticationFilter,重寫了其中的2個方法
-
* attemptAuthentication :接收並解析用戶憑證。
-
* successfulAuthentication :用戶成功登錄后,這個方法會被調用,我們在這個方法里生成token。
-
* @author zhaoxinguo on 2017/9/12.
-
*/
-
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
-
-
private AuthenticationManager authenticationManager;
-
-
public JWTLoginFilter(AuthenticationManager authenticationManager) {
-
this.authenticationManager = authenticationManager;
-
}
-
-
// 接收並解析用戶憑證
-
-
public Authentication attemptAuthentication(HttpServletRequest req,
-
HttpServletResponse res) throws AuthenticationException {
-
try {
-
User user = new ObjectMapper()
-
.readValue(req.getInputStream(), User.class);
-
-
return authenticationManager.authenticate(
-
new UsernamePasswordAuthenticationToken(
-
user.getUsername(),
-
user.getPassword(),
-
new ArrayList<>())
-
);
-
} catch (IOException e) {
-
throw new RuntimeException(e);
-
}
-
}
-
-
// 用戶成功登錄后,這個方法會被調用,我們在這個方法里生成token
-
-
protected void successfulAuthentication(HttpServletRequest req,
-
HttpServletResponse res,
-
FilterChain chain,
-
Authentication auth) throws IOException, ServletException {
-
-
String token = Jwts.builder()
-
.setSubject(((org.springframework.security.core.userdetails.User) auth.getPrincipal()).getUsername())
-
.setExpiration( new Date(System.currentTimeMillis() + 60 * 60 * 24 * 1000))
-
.signWith(SignatureAlgorithm.HS512, "MyJwtSecret")
-
.compact();
-
res.addHeader( "Authorization", "Bearer " + token);
-
}
-
-
}
該類繼承自UsernamePasswordAuthenticationFilter,重寫了其中的2個方法:
attemptAuthentication
:接收並解析用戶憑證。
successfulAuthentication
:用戶成功登錄后,這個方法會被調用,我們在這個方法里生成token。
授權驗證
用戶一旦登錄成功后,會拿到token,后續的請求都會帶着這個token,服務端會驗證token的合法性。
創建JWTAuthenticationFilter類,我們在這個類中實現token的校驗功能。
-
package boss.portal.web.filter;
-
-
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;
-
-
/**
-
* token的校驗
-
* 該類繼承自BasicAuthenticationFilter,在doFilterInternal方法中,
-
* 從http頭的Authorization 項讀取token數據,然后用Jwts包提供的方法校驗token的合法性。
-
* 如果校驗通過,就認為這是一個取得授權的合法請求
-
* @author zhaoxinguo on 2017/9/13.
-
*/
-
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
-
-
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
-
super(authenticationManager);
-
}
-
-
-
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
-
String header = request.getHeader( "Authorization");
-
-
if (header == null || !header.startsWith("Bearer ")) {
-
chain.doFilter(request, response);
-
return;
-
}
-
-
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
-
-
SecurityContextHolder.getContext().setAuthentication(authentication);
-
chain.doFilter(request, response);
-
-
}
-
-
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
-
String token = request.getHeader( "Authorization");
-
if (token != null) {
-
// parse the token.
-
String user = Jwts.parser()
-
.setSigningKey( "MyJwtSecret")
-
.parseClaimsJws(token.replace( "Bearer ", ""))
-
.getBody()
-
.getSubject();
-
-
if (user != null) {
-
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
-
}
-
return null;
-
}
-
return null;
-
}
-
-
}
該類繼承自BasicAuthenticationFilter,在doFilterInternal方法中,從http頭的Authorization
項讀取token數據,然后用Jwts包提供的方法校驗token的合法性。如果校驗通過,就認為這是一個取得授權的合法請求。
SpringSecurity配置
通過SpringSecurity的配置,將上面的方法組合在一起。
-
package boss.portal.security;
-
-
import boss.portal.web.filter.JWTLoginFilter;
-
import boss.portal.web.filter.JWTAuthenticationFilter;
-
import org.springframework.boot.autoconfigure.security.SecurityProperties;
-
import org.springframework.context.annotation.Configuration;
-
import org.springframework.core.annotation.Order;
-
import org.springframework.http.HttpMethod;
-
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
-
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
-
import org.springframework.security.core.userdetails.UserDetailsService;
-
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
-
-
/**
-
* SpringSecurity的配置
-
* 通過SpringSecurity的配置,將JWTLoginFilter,JWTAuthenticationFilter組合在一起
-
* @author zhaoxinguo on 2017/9/13.
-
*/
-
-
-
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
-
-
private UserDetailsService userDetailsService;
-
-
private BCryptPasswordEncoder bCryptPasswordEncoder;
-
-
public WebSecurityConfig(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
-
this.userDetailsService = userDetailsService;
-
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
-
}
-
-
-
protected void configure(HttpSecurity http) throws Exception {
-
http.cors().and().csrf().disable().authorizeRequests()
-
.antMatchers(HttpMethod.POST, "/users/signup").permitAll()
-
.anyRequest().authenticated()
-
.and()
-
.addFilter( new JWTLoginFilter(authenticationManager()))
-
.addFilter( new JWTAuthenticationFilter(authenticationManager()));
-
}
-
-
-
public void configure(AuthenticationManagerBuilder auth) throws Exception {
-
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
-
}
-
-
}
這是標准的SpringSecurity配置內容,就不在詳細說明。注意其中的
.addFilter(new JWTLoginFilter(authenticationManager()))
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
這兩行,將我們定義的JWT方法加入SpringSecurity的處理流程中。
下面對我們的程序進行簡單的驗證:
# 請求hello接口,會收到403錯誤,如下圖:
curl http://localhost:8080/hello
# 注冊一個新用戶curl -H"Content-Type: application/json" -X POST -d '{"username":"admin","password":"password"}' http://localhost:8080/users/signup
如下圖:
# 登錄,會返回token,在http header中,Authorization: Bearer 后面的部分就是tokencurl -i -H"Content-Type: application/json" -X POST -d '{"username":"admin","password":"password"}' http://localhost:8080/login
如下圖:
# 用登錄成功后拿到的token再次請求hello接口# 將請求中的XXXXXX替換成拿到的token# 這次可以成功調用接口了curl -H"Content-Type: application/json" \-H"Authorization: Bearer XXXXXX" \"http://localhost:8080/users/hello"
如下圖:
五:總結
至此,給SpringBoot的接口加上JWT認證的功能就實現了,過程並不復雜,主要是開發兩個SpringSecurity的filter,來生成和校驗JWT token。
JWT作為一個無狀態的授權校驗技術,非常適合於分布式系統架構,因為服務端不需要保存用戶狀態,因此就無需采用redis等技術,在各個服務節點之間共享session數據。
六:源碼下載地址:
地址: https://gitee.com/micai/springboot-springsecurity-jwt-demo若您有任何建議,可以通過1)加入qq群715224124向群主提出,或2)發送郵件至827358369@qq.com向我反饋。本人承諾,任何建議都將會被認真考慮,優秀的建議將會被采用,但不保證一定會在當前版本中實現。
八:鳴謝地址:
http://blog.csdn.net/haiyan_qi/article/details/77373900
https://segmentfault.com/a/1190000009231329