做者 freewolfhtml
原創文章轉載請標明出處git
Spring Boot
、OAuth 2.0
、JWT
、Spring Security
、SSO
、UAA
github
最近安靜下來,從新學習一些東西,最近一年幾乎沒寫過代碼。成天疲於奔命的日子終於結束了。坐下來,弄杯咖啡,思考一些問題,挺好。這幾天有人問我Spring Boot結合Spring Security實現OAuth認證的問題,寫了個Demo,順便分享下。Spring 2以後就沒再用過Java,主要是xml太麻煩,就投入了Node.js的懷抱,如今Java卻是好過以前不少,不管是執行效率仍是其餘什麼。感謝Pivotal團隊在Spring boot上的努力,感謝Josh Long,一個有意思的攻城獅。web
我又搞Java也是爲了去折騰微服務,由於目前看國內就Java程序猿最好找,雖然水平好的難找,可是至少能找到,不像其餘編程語言,找個會世界上最好的編程語言PHP的人真的不易。spring
有了Spring Boot這樣的神器,能夠很簡單的使用強大的Spring框架。你須要關心的事兒只是建立應用,沒必要再配置了,「Just run!」,這但是Josh Long
每次演講必說的,他的另外一句必須說的就是「make jar not war」
,這意味着,不用太關心是Tomcat仍是Jetty或者Undertow了。專心解決邏輯問題,這固然是個好事兒,部署簡單了不少。數據庫
有不少方法去建立Spring Boot項目,官方也推薦用:編程
CLI 工具api
start.spring.io
能夠方便選擇你要用的組件,命令行工具固然也能夠。目前Spring Boot已經到了1.53,我是懶得去更新依賴,繼續用1.52版本。雖然阿里也有了中央庫的國內版本不知道是否穩定。若是你感興趣,能夠本身嘗試下。你能夠選Maven或者Gradle成爲你項目的構建工具,Gradle優雅一些,使用了Groovy語言進行描述。瀏覽器
打開start.spring.io
,建立的項目只須要一個Dependency,也就是Web,而後下載項目,用IntellJ IDEA
打開。個人Java版本是1.8。
這裏看下整個項目的pom.xml
文件中的依賴部分:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
全部Spring Boot相關的依賴都是以starter形式出現,這樣你無需關心版本和相關的依賴,因此這樣大大簡化了開發過程。
當你在pom文件中集成了spring-boot-maven-plugin插件後你可使用Maven相關的命令來run你的應用。例如mvn spring-boot:run
,這樣會啓動一個嵌入式的Tomcat,並運行在8080端口,直接訪問你固然會得到一個Whitelabel Error Page
,這說明Tomcat已經啓動了。
這仍是一篇關於Web安全的文章,可是也得先有個簡單的HTTP請求響應。咱們先弄一個能夠返回JSON的Controller。修改程序的入口文件:
@SpringBootApplication @RestController @EnableAutoConfiguration public class DemoApplication { // main函數,Spring Boot程序入口 public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } // 根目錄映射 Get訪問方式 直接返回一個字符串 @RequestMapping("/") Map<String, String> hello() { // 返回map會變成JSON key value方式 Map<String,String> map=new HashMap<String,String>(); map.put("content", "hello freewolf~"); return map; } }
這裏我儘可能的寫清楚,讓不瞭解Spring Security的人經過這個例子能夠了解這個東西,不少人都以爲它很複雜,而投向了Apache Shiro
,其實這個並不難懂。知道主要的處理流程,和這個流程中哪些類都起了哪些做用就行了。
Spring Boot
對於開發人員最大的好處在於能夠對Spring
應用進行自動配置。Spring Boot
會根據應用中聲明的第三方依賴來自動配置Spring
框架,而不須要進行顯式的聲明。Spring Boot
推薦採用基於Java
註解的配置方式,而不是傳統的XML
。只須要在主配置 Java 類上添加@EnableAutoConfiguration
註解就能夠啓用自動配置。Spring Boot
的自動配置功能是沒有侵入性的,只是做爲一種基本的默認實現。
這個入口類咱們添加@RestController
和@EnableAutoConfiguration
兩個註解。@RestController
註解至關於@ResponseBody
和@Controller
合在一塊兒的做用。
run整個項目。訪問http://localhost:8080/
就能看到這個JSON的輸出。使用Chrome瀏覽器能夠裝JSON Formatter這個插件,顯示更PL
一些。
{ "content": "hello freewolf~" }
爲了顯示統一的JSON返回,這裏創建一個JSONResult類進行,簡單的處理。首先修改pom.xml,加入org.json
相關依賴。
<dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> </dependency>
而後在咱們的代碼中加入一個新的類,裏面只有一個結果集處理方法,由於只是個Demo,全部這裏都放在一個文件中。這個類只是讓返回的JSON結果變爲三部分:
status - 返回狀態碼 0 表明正常返回,其餘都是錯誤
message - 通常顯示錯誤信息
result - 結果集
class JSONResult{ public static String fillResultString(Integer status, String message, Object result){ JSONObject jsonObject = new JSONObject(){{ put("status", status); put("message", message); put("result", result); }}; return jsonObject.toString(); } }
而後咱們引入一個新的@RestController
並返回一些簡單的結果,後面咱們將對這些內容進行訪問控制,這裏用到了上面的結果集處理類。這裏多放兩個方法,後面咱們來測試權限和角色的驗證用。
@RestController class UserController { // 路由映射到/users @RequestMapping(value = "/users", produces="application/json;charset=UTF-8") public String usersList() { ArrayList<String> users = new ArrayList<String>(){{ add("freewolf"); add("tom"); add("jerry"); }}; return JSONResult.fillResultString(0, "", users); } @RequestMapping(value = "/hello", produces="application/json;charset=UTF-8") public String hello() { ArrayList<String> users = new ArrayList<String>(){{ add("hello"); }}; return JSONResult.fillResultString(0, "", users); } @RequestMapping(value = "/world", produces="application/json;charset=UTF-8") public String world() { ArrayList<String> users = new ArrayList<String>(){{ add("world"); }}; return JSONResult.fillResultString(0, "", users); } }
從新run這個文件,訪問http://localhost:8080/users
就看到了下面的結果:
{ "result": [ "freewolf", "tom", "jerry" ], "message": "", "status": 0 }
若是你細心,你會發現這裏的JSON返回時,Chrome的格式化插件好像並無識別?這是爲何呢?咱們藉助curl
分別看一下咱們寫的兩個方法的Header
信息.
curl -I http://127.0.0.1:8080/ curl -I http://127.0.0.1:8080/users
能夠看到第一個方法hello
,因爲返回值是Map<String, String>,Spring已經有相關的機制自動處理成JSON:
Content-Type: application/json;charset=UTF-8
第二個方法usersList
因爲返回時String,因爲是@RestControler
已經含有了@ResponseBody
也就是直接返回內容,並不模板。因此就是:
Content-Type: text/plain;charset=UTF-8
那怎麼才能讓它變成JSON呢,其實也很簡單隻須要補充一下相關注解:
@RequestMapping(value = "/users", produces="application/json;charset=UTF-8")
這樣就行了。
終於咱們開始介紹正題,這裏咱們會對/users
進行訪問控制,先經過申請一個JWT(JSON Web Token讀jot)
,而後經過這個訪問/users,才能拿到數據。
關於JWT
,出門奔向如下內容,這些不在本文討論範圍內:
JWT
很大程度上仍是個新技術,經過使用HMAC(Hash-based Message Authentication Code)
計算信息摘要,也能夠用RSA公私鑰中的私鑰進行簽名。這個根據業務場景進行選擇。
根據上文咱們說過咱們要對/users
進行訪問控制,讓用戶在/login
進行登陸並得到Token
。這裏咱們須要將spring-boot-starter-security
加入pom.xml
。加入後,咱們的Spring Boot
項目將須要提供身份驗證,相關的pom.xml
以下:
<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>
至此咱們以前全部的路由都須要身份驗證。咱們將引入一個安全設置類WebSecurityConfig
,這個類須要從WebSecurityConfigurerAdapter
類繼承。
@Configuration @EnableWebSecurity class WebSecurityConfig extends WebSecurityConfigurerAdapter { // 設置 HTTP 驗證規則 @Override protected void configure(HttpSecurity http) throws Exception { // 關閉csrf驗證 http.csrf().disable() // 對請求進行認證 .authorizeRequests() // 全部 / 的全部請求 都放行 .antMatchers("/").permitAll() // 全部 /login 的POST請求 都放行 .antMatchers(HttpMethod.POST, "/login").permitAll() // 權限檢查 .antMatchers("/hello").hasAuthority("AUTH_WRITE") // 角色檢查 .antMatchers("/world").hasRole("ADMIN") // 全部請求須要身份認證 .anyRequest().authenticated() .and() // 添加一個過濾器 全部訪問 /login 的請求交給 JWTLoginFilter 來處理 這個類處理全部的JWT相關內容 .addFilterBefore(new JWTLoginFilter("/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class) // 添加一個過濾器驗證其餘請求的Token是否合法 .addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 使用自定義身份驗證組件 auth.authenticationProvider(new CustomAuthenticationProvider()); } }
先放兩個基本類,一個負責存儲用戶名密碼,另外一個是一個權限類型,負責存儲權限和角色。
class AccountCredentials { private String username; private String password; 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; } } class GrantedAuthorityImpl implements GrantedAuthority{ private String authority; public GrantedAuthorityImpl(String authority) { this.authority = authority; } public void setAuthority(String authority) { this.authority = authority; } @Override public String getAuthority() { return this.authority; } }
在上面的安全設置類中,咱們設置全部人都能訪問/
和POST
方式訪問/login
,其餘的任何路由都須要進行認證。而後將全部訪問/login
的請求,都交給JWTLoginFilter
過濾器來處理。稍後咱們會建立這個過濾器和其餘這裏須要的JWTAuthenticationFilter
和CustomAuthenticationProvider
兩個類。
先創建一個JWT生成,和驗籤的類
class TokenAuthenticationService { static final long EXPIRATIONTIME = 432_000_000; // 5天 static final String SECRET = "P@ssw02d"; // JWT密碼 static final String TOKEN_PREFIX = "Bearer"; // Token前綴 static final String HEADER_STRING = "Authorization";// 存放Token的Header Key // JWT生成方法 static void addAuthentication(HttpServletResponse response, String username) { // 生成JWT String JWT = Jwts.builder() // 保存權限(角色) .claim("authorities", "ROLE_ADMIN,AUTH_WRITE") // 用戶名寫入標題 .setSubject(username) // 有效期設置 .setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME)) // 簽名設置 .signWith(SignatureAlgorithm.HS512, SECRET) .compact(); // 將 JWT 寫入 body try { response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_OK); response.getOutputStream().println(JSONResult.fillResultString(0, "", JWT)); } catch (IOException e) { e.printStackTrace(); } } // JWT驗證方法 static Authentication getAuthentication(HttpServletRequest request) { // 從Header中拿到token String token = request.getHeader(HEADER_STRING); if (token != null) { // 解析 Token Claims claims = Jwts.parser() // 驗籤 .setSigningKey(SECRET) // 去掉 Bearer .parseClaimsJws(token.replace(TOKEN_PREFIX, "")) .getBody(); // 拿用戶名 String user = claims.getSubject(); // 獲得 權限(角色) List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities")); // 返回驗證令牌 return user != null ? new UsernamePasswordAuthenticationToken(user, null, authorities) : null; } return null; } }
這個類就兩個static
方法,一個負責生成JWT,一個負責認證JWT最後生成驗證令牌。註釋已經寫得很清楚了,這裏很少說了。
下面來看自定義驗證組件,這裏簡單寫了,這個類就是提供密碼驗證功能,在實際使用時換成本身相應的驗證邏輯,從數據庫中取出、比對、賦予用戶相應權限。
// 自定義身份認證驗證組件 class CustomAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 獲取認證的用戶名 & 密碼 String name = authentication.getName(); String password = authentication.getCredentials().toString(); // 認證邏輯 if (name.equals("admin") && password.equals("123456")) { // 這裏設置權限和角色 ArrayList<GrantedAuthority> authorities = new ArrayList<>(); authorities.add( new GrantedAuthorityImpl("ROLE_ADMIN") ); authorities.add( new GrantedAuthorityImpl("AUTH_WRITE") ); // 生成令牌 Authentication auth = new UsernamePasswordAuthenticationToken(name, password, authorities); return auth; }else { throw new BadCredentialsException("密碼錯誤~"); } } // 是否能夠提供輸入類型的認證服務 @Override public boolean supports(Class<?> authentication) { return authentication.equals(UsernamePasswordAuthenticationToken.class); } }
下面實現JWTLoginFilter
這個Filter比較簡單,除了構造函數須要重寫三個方法。
attemptAuthentication - 登陸時須要驗證時候調用
successfulAuthentication - 驗證成功後調用
unsuccessfulAuthentication - 驗證失敗後調用,這裏直接灌入500錯誤返回,因爲同一JSON返回,HTTP就都返回200了
class JWTLoginFilter extends AbstractAuthenticationProcessingFilter { public JWTLoginFilter(String url, AuthenticationManager authManager) { super(new AntPathRequestMatcher(url)); setAuthenticationManager(authManager); } @Override public Authentication attemptAuthentication( HttpServletRequest req, HttpServletResponse res) throws AuthenticationException, IOException, ServletException { // JSON反序列化成 AccountCredentials AccountCredentials creds = new ObjectMapper().readValue(req.getInputStream(), AccountCredentials.class); // 返回一個驗證令牌 return getAuthenticationManager().authenticate( new UsernamePasswordAuthenticationToken( creds.getUsername(), creds.getPassword() ) ); } @Override protected void successfulAuthentication( HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException { TokenAuthenticationService.addAuthentication(res, auth.getName()); } @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_OK); response.getOutputStream().println(JSONResult.fillResultString(500, "Internal Server Error!!!", JSONObject.NULL)); } }
再完成最後一個類JWTAuthenticationFilter
,這也是個攔截器,它攔截全部須要JWT
的請求,而後調用TokenAuthenticationService
類的靜態方法去作JWT
驗證。
class JWTAuthenticationFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { Authentication authentication = TokenAuthenticationService .getAuthentication((HttpServletRequest)request); SecurityContextHolder.getContext() .setAuthentication(authentication); filterChain.doFilter(request,response); } }
如今代碼就寫完了,整個Spring Security
結合JWT
基本就差很少了,下面咱們來測試下,並說下總體流程。
開始測試,先運行整個項目,這裏介紹下過程:
先程序啓動 - main函數
註冊驗證組件 - WebSecurityConfig
類 configure(AuthenticationManagerBuilder auth)
方法,這裏咱們註冊了自定義驗證組件
設置驗證規則 - WebSecurityConfig
類 configure(HttpSecurity http)
方法,這裏設置了各類路由訪問規則
初始化過濾組件 - JWTLoginFilter
和 JWTAuthenticationFilter
類會初始化
首先測試獲取Token,這裏使用CURL命令行工具來測試。
curl -H "Content-Type: application/json" -X POST -d '{"username":"admin","password":"123456"}' http://127.0.0.1:8080/login
結果:
{ "result": "eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sQVVUSF9XUklURSIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDkzNzgyMjQwfQ.HNfV1CU2CdAnBTH682C5-KOfr2P71xr9PYLaLpDVhOw8KWWSJ0lBo0BCq4LoNwsK_Y3-W3avgbJb0jW9FNYDRQ", "message": "", "status": 0 }
這裏咱們獲得了相關的JWT
,反Base64以後,就是下面的內容,標準JWT
。
{"alg":"HS512"}{"authorities":"ROLE_ADMIN,AUTH_WRITE","sub":"admin","exp":1493782240}ͽ]BS`pS6~hCVH% ܬ)֝ଖoE5р
整個過程以下:
拿到傳入JSON,解析用戶名密碼 - JWTLoginFilter
類 attemptAuthentication
方法
自定義身份認證驗證組件,進行身份認證 - CustomAuthenticationProvider
類 authenticate
方法
鹽城成功 - JWTLoginFilter
類 successfulAuthentication
方法
生成JWT - TokenAuthenticationService
類 addAuthentication
方法
再測試一個訪問資源的:
curl -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sQVVUSF9XUklURSIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDkzNzgyMjQwfQ.HNfV1CU2CdAnBTH682C5-KOfr2P71xr9PYLaLpDVhOw8KWWSJ0lBo0BCq4LoNwsK_Y3-W3avgbJb0jW9FNYDRQ" http://127.0.0.1:8080/users
結果:
{ "result":["freewolf","tom","jerry"], "message":"", "status":0 }
說明咱們的Token生效能夠正常訪問。其餘的結果您能夠本身去測試。再回處處理流程:
接到請求進行攔截 - JWTAuthenticationFilter
中的方法
驗證JWT - TokenAuthenticationService
類 getAuthentication
方法
訪問Controller
這樣本文的主要流程就結束了,本文主要介紹了,如何用Spring Security
結合JWT
保護你的Spring Boot
應用。如何使用Role
和Authority
,這裏多說一句其實在Spring Security
中,對於GrantedAuthority
接口實現類來講是不區分是Role
仍是Authority
,兩者區別就是若是是hasAuthority
判斷,就是判斷整個字符串,判斷hasRole
時,系統自動加上ROLE_
到判斷的Role
字符串上,也就是說hasRole("CREATE")
和hasAuthority('ROLE_CREATE')
是相同的。利用這些能夠搭建完整的RBAC
體系。本文到此,你已經會用了本文介紹的知識點。
代碼整理後我會上傳到Github
https://github.com/freew01f/s...