在先後端分離的項目中,登陸策略也有很多,不過 JWT 算是目前比較流行的一種解決方案了,本文就和你們來分享一下如何將 Spring Security 和 JWT 結合在一塊兒使用,進而實現先後端分離時的登陸解決方案。前端
有狀態服務,即服務端須要記錄每次會話的客戶端信息,從而識別客戶端身份,根據用戶身份進行請求的處理,典型的設計如Tomcat中的Session。例如登陸:用戶登陸後,咱們把用戶的信息保存在服務端session中,而且給用戶一個cookie值,記錄對應的session,而後下次請求,用戶攜帶cookie值來(這一步有瀏覽器自動完成),咱們就能識別到對應session,從而找到用戶的信息。這種方式目前來看最方便,可是也有一些缺陷,以下:java
微服務集羣中的每一個服務,對外提供的都使用RESTful風格的接口。而RESTful風格的一個最重要的規範就是:服務的無狀態性,即:git
那麼這種無狀態性有哪些好處呢?github
無狀態登陸的流程:web
JWT,全稱是Json Web Token, 是一種JSON風格的輕量級的受權和身份認證規範,可實現無狀態、分佈式的Web應用受權: redis
JWT 做爲一種規範,並無和某一種語言綁定在一塊兒,經常使用的Java 實現是GitHub 上的開源項目 jjwt,地址以下:https://github.com/jwtk/jjwt
算法
JWT包含三部分數據:spring
Header:頭部,一般頭部有兩部分信息:數據庫
咱們會對頭部進行Base64Url編碼(可解碼),獲得第一部分數據。json
Payload:載荷,就是有效數據,在官方文檔中(RFC7519),這裏給了7個示例信息:
這部分也會採用Base64Url編碼,獲得第二部分數據。
生成的數據格式以下圖:
注意,這裏的數據經過 .
隔開成了三部分,分別對應前面提到的三部分,另外,這裏數據是不換行的,圖片換行只是爲了展現方便而已。
流程圖:
步驟翻譯:
由於JWT簽發的token中已經包含了用戶的身份信息,而且每次請求都會攜帶,這樣服務的就無需保存用戶信息,甚至無需去數據庫查詢,這樣就徹底符合了RESTful的無狀態規範。
說了這麼多,JWT 也不是完美無缺,由客戶端維護登陸狀態帶來的一些問題在這裏依然存在,舉例以下:
說了這麼久,接下來咱們就來看看這個東西到底要怎麼用?
首先咱們來建立一個Spring Boot項目,建立時須要添加Spring Security依賴,建立完成後,添加 jjwt
依賴,完整的pom.xml文件以下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
而後在項目中建立一個簡單的 User 對象實現 UserDetails 接口,以下:
public class User implements UserDetails { private String username; private String password; private List<GrantedAuthority> authorities; public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } //省略getter/setter }
這個就是咱們的用戶對象,先放着備用,再建立一個HelloController,內容以下:
@RestController public class HelloController { @GetMapping("/hello") public String hello() { return "hello jwt !"; } @GetMapping("/admin") public String admin() { return "hello admin !"; } }
HelloController 很簡單,這裏有兩個接口,設計是 /hello
接口能夠被具備 user 角色的用戶訪問,而 /admin
接口則能夠被具備 admin 角色的用戶訪問。
接下來提供兩個和 JWT 相關的過濾器配置:
這兩個過濾器,咱們分別來看,先看第一個:
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter { protected JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) { super(new AntPathRequestMatcher(defaultFilterProcessesUrl)); setAuthenticationManager(authenticationManager); } @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException, IOException, ServletException { User user = new ObjectMapper().readValue(req.getInputStream(), User.class); return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())); } @Override protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException { Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities(); StringBuffer as = new StringBuffer(); for (GrantedAuthority authority : authorities) { as.append(authority.getAuthority()) .append(","); } String jwt = Jwts.builder() .claim("authorities", as)//配置用戶角色 .setSubject(authResult.getName()) .setExpiration(new Date(System.currentTimeMillis() + 10 * 60 * 1000)) .signWith(SignatureAlgorithm.HS512,"sang@123") .compact(); resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); out.write(new ObjectMapper().writeValueAsString(jwt)); out.flush(); out.close(); } protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); out.write("登陸失敗!"); out.flush(); out.close(); } }
關於這個類,我說以下幾點:
,
鏈接起來,而後再利用Jwts去生成token,按照代碼的順序,生成過程一共配置了四個參數,分別是用戶角色、主題、過時時間以及加密算法和密鑰,而後將生成的token寫出到客戶端。再來看第二個token校驗的過濾器:
public class JwtFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; String jwtToken = req.getHeader("authorization"); System.out.println(jwtToken); Claims claims = Jwts.parser().setSigningKey("sang@123").parseClaimsJws(jwtToken.replace("Bearer","")) .getBody(); String username = claims.getSubject();//獲取當前登陸用戶名 List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities")); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities); SecurityContextHolder.getContext().setAuthentication(token); filterChain.doFilter(req,servletResponse); } }
關於這個過濾器,我說以下幾點:
如此以後,兩個和JWT相關的過濾器就算配置好了。
接下來咱們來配置 Spring Security,以下:
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("admin") .password("123").roles("admin") .and() .withUser("sang") .password("456") .roles("user"); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/hello").hasRole("user") .antMatchers("/admin").hasRole("admin") .antMatchers(HttpMethod.POST, "/login").permitAll() .anyRequest().authenticated() .and() .addFilterBefore(new JwtLoginFilter("/login",authenticationManager()),UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new JwtFilter(),UsernamePasswordAuthenticationFilter.class) .csrf().disable(); } }
/hello
接口必需要具有 user 角色才能訪問, /admin
接口必需要具有 admin 角色才能訪問,POST 請求而且是 /login
接口則能夠直接經過,其餘接口必須認證後才能訪問。作完這些以後,咱們的環境就算徹底搭建起來了,接下來啓動項目而後在 POSTMAN 中進行測試,以下:
登陸成功後返回的字符串就是通過 base64url 轉碼的token,一共有三部分,經過一個 .
隔開,咱們能夠對第一個 .
以前的字符串進行解碼,即Header,以下:
再對兩個 .
之間的字符解碼,即 payload:
能夠看到,咱們設置信息,因爲base64並非加密方案,只是一種編碼方案,所以,不建議將敏感的用戶信息放到token中。
接下來再去訪問 /hello
接口,注意認證方式選擇 Bearer Token,Token值爲剛剛獲取到的值,以下:
能夠看到,訪問成功。
這就是 JWT 結合 Spring Security 的一個簡單用法,講真,若是實例容許,相似的需求我仍是推薦使用 OAuth2 中的 password 模式。
不知道大夥有沒有看懂呢?若是沒看懂,鬆哥還有一個關於這個知識點的視頻教程,以下:
如何獲取這個視頻教程呢?很簡單,將本文轉發到一個超過100人的微信羣中(QQ羣不算,鬆哥是羣主的微信羣也不算,羣要爲Java方向),或者多個微信羣中,只要累計人數達到100人便可,而後加鬆哥微信,截圖發給鬆哥便可獲取資料。
關注公衆號【江南一點雨】,專一於 Spring Boot+微服務以及先後端分離等全棧技術,按期視頻教程分享,關注後回覆 Java ,領取鬆哥爲你精心準備的 Java 乾貨!