前面整理過一篇 SpringBoot Security前后端分離,登錄退出等返回json數據,也就是用Spring Security,基於SpringBoot2.1.4 RELEASE前后端分離的情況下,實現了登陸登出的功能,亮點就在於以JSON的形式接收返回參數。這個是針對單個后台服務的, 登錄信息都存儲在SecurityContextHolder緩存里。如果是兩個或兩個以上的應用呢,那該怎么辦?Session是不能用了,Cookie自然也不能用,畢竟它倆是一對的。
曾想過用OAuth2來解決這個問題,但是OAuth2太復雜,首先理解概念就需要花費一些時間,而且里面的授權服務器、資源服務器、客戶端等等讓人傻傻分不清,還有四種授權模式,要反復衡量,到底要用哪一種,概念還沒有扯清楚就開始糾結使用哪一個了。從概念入手不是個好主意,也不是個輕松的主意。在理解OAuth2的過程中,想到自己的項目是前后端分離的,離不開JSON,無意中遇見JWT。JWT是什么玩意,咦,難道是自己苦苦尋求的嗎?!
那么,什么是JWT呢?看看專家介紹 阮一峰的網絡日志,才知道,JWT 是JSON Web Token的簡稱,它解決的就是跨域問題。看來,要找的就是它,簡單的,但也是管用的。
繼續深究,JWT到底是怎樣和SpringSecurity結合的呢。下面上代碼,在上代碼前先說明一下,在本次實例中,涉及到兩個項目,一個項目是登錄的項目A,另一個項目是根據token進行訪問的項目B。其中B項目沒有登錄,也不會涉及登錄,只要有Token就可以訪問,Token失效了就訪問不了了。
A項目是登錄的項目,也是一個只能通過登錄進行訪問的后台服務。B項目就是一個服務,只要用戶在A項目登錄了,就可以訪問。

A項目配置,代碼如下
第一步,A項目 POM.xml 引入文件
<!-- 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.9.0</version> </dependency>
第二步,A項目SecurityConfig配置
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; 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.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import com.example.demo.filter.JWTAuthenticationFilter; import com.example.demo.filter.JWTLoginFilter; /** * SpringSecurity的配置 * 參考網址:https://blog.csdn.net/sxdtzhaoxinguo/article/details/77965226 * @author 程就人生 * @date 2019年5月26日 */ @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService myCustomUserService; @Autowired private MyPasswordEncoder myPasswordEncoder; @Override protected void configure(HttpSecurity http) throws Exception { http //關閉跨站請求防護 .cors().and().csrf().disable() //允許不登陸就可以訪問的方法,多個用逗號分隔 .authorizeRequests().antMatchers("/test").permitAll() //其他的需要授權后訪問 .anyRequest().authenticated() .and() //增加登錄攔截 .addFilter(new JWTLoginFilter(authenticationManager())) //增加是否登陸過濾 .addFilter(new JWTAuthenticationFilter(authenticationManager())) // 前后端分離是無狀態的,所以暫時不用session,將登陸信息保存在token中。 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { //覆蓋UserDetailsService類 auth.userDetailsService(myCustomUserService) //覆蓋默認的密碼驗證類 .passwordEncoder(myPasswordEncoder); } }
第三步,實現配置文件中自定義的類
- MyPasswordEncoder類實現了默認的PasswordEncoder 接口,可以對密碼加密和密碼對比進行個性化定制
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; /** * 自定義的密碼加密方法,實現了PasswordEncoder接口 * @author 程就人生 * @date 2019年5月26日 */ @Component public class MyPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence charSequence) { //加密方法可以根據自己的需要修改 return charSequence.toString(); } @Override public boolean matches(CharSequence charSequence, String s) { return encode(charSequence).equals(s); } }
- MyCustomUserService 實現了框架默認的UserDetailsService,可以根據username從數據庫獲取用戶,查看用戶是否存在
/** * 登錄專用類,用戶登陸時,通過這里查詢數據庫 * 自定義類,實現了UserDetailsService接口,用戶登錄時調用的第一類 * @author 程就人生 * @date 2019年5月26日 */ @Component public class MyCustomUserService implements UserDetailsService { /** * 登陸驗證時,通過username獲取用戶的所有權限信息 * 並返回UserDetails放到spring的全局緩存SecurityContextHolder中,以供授權器使用 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //在這里可以自己調用數據庫,對username進行查詢,看看在數據庫中是否存在 MyUserDetails myUserDetail = new MyUserDetails(); myUserDetail.setUsername(username); myUserDetail.setPassword("123456"); return myUserDetail; } }
- MyUserDetails 實現了框架的UserDetails接口,可以在該類中根據需要添加自己必需的屬性
import java.util.Collection; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; /** * 實現了UserDetails接口,只留必需的屬性,也可添加自己需要的屬性 * @author 程就人生 * @date 2019年5月26日 */ public class MyUserDetails implements UserDetails { private static final long serialVersionUID = 1L; //登錄用戶名 private String username; //登錄密碼 private String password; private Collection<? extends GrantedAuthority> authorities; public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setAuthorities(Collection<? extends GrantedAuthority> authorities) { this.authorities = authorities; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.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; } }
- JWTLoginFilter 實現了框架自帶的UsernamePasswordAuthenticationFilter 接口,對攔截做處理,以便登錄成功后,在頭部設置token返回;不管登錄成功還是失敗,都有JSON數據返回
import java.io.PrintWriter; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Map; import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import com.example.demo.entity.User; import com.example.demo.security.MyUserDetails; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; /** * 驗證用戶名密碼正確后,生成一個token,放在header里,返回給客戶端 * @author 程就人生 * @date 2019年5月26日 */ public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; public JWTLoginFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } /** * 接收並解析用戶憑證,出現錯誤時,返回json數據前端 */ @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res){ try { User user =new ObjectMapper().readValue(req.getInputStream(), User.class); return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( user.getUsername(), user.getPassword(), new ArrayList<>()) ); } catch (Exception e) { try { //未登錄出現賬號或密碼錯誤時,使用json進行提示 res.setContentType("application/json;charset=utf-8"); res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); PrintWriter out = res.getWriter(); Map<String,Object> map = new HashMap<String,Object>(); map.put("code",HttpServletResponse.SC_UNAUTHORIZED); map.put("message","賬號或密碼錯誤!"); out.write(new ObjectMapper().writeValueAsString(map)); out.flush(); out.close(); } catch (Exception e1) { e1.printStackTrace(); } throw new RuntimeException(e); } } /** * 用戶登錄成功后,生成token,並且返回json數據給前端 */ @Override protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res,FilterChain chain, Authentication auth){ //json web token構建 String token = Jwts.builder() //此處為自定義的、實現org.springframework.security.core.userdetails.UserDetails的類,需要和配置中設置的保持一致 //此處的subject可以用一個用戶名,也可以是多個信息的組合,根據需要來定 .setSubject(((MyUserDetails) auth.getPrincipal()).getUsername()) //設置token過期時間,24小時 .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 24 * 1000)) //設置token簽名、密鑰 .signWith(SignatureAlgorithm.HS512, "MyJwtSecret") .compact(); //返回token res.addHeader("Authorization", "Bearer " + token); try { //登錄成功時,返回json格式進行提示 res.setContentType("application/json;charset=utf-8"); res.setStatus(HttpServletResponse.SC_OK); PrintWriter out = res.getWriter(); Map<String,Object> map = new HashMap<String,Object>(); map.put("code",HttpServletResponse.SC_OK); map.put("message","登陸成功!"); out.write(new ObjectMapper().writeValueAsString(map)); out.flush(); out.close(); } catch (Exception e1) { e1.printStackTrace(); } } }
- JWTAuthenticationFilter 類實現了BasicAuthenticationFilter 接口,對Controller中需要登錄后才能訪問的方法進行了攔截,沒有登錄,則不能訪問,返回JSON信息進行提示
import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; 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 com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Jwts; /** * 是否登陸驗證方法 * @author 程就人生 * @date 2019年5月26日 */ public class JWTAuthenticationFilter extends BasicAuthenticationFilter { public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } /** * 對請求進行過濾 */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { try { //請求體的頭中是否包含Authorization String header = request.getHeader("Authorization"); //Authorization中是否包含Bearer,有一個不包含時直接返回 if (header == null || !header.startsWith("Bearer ")) { chain.doFilter(request, response); responseJson(response); return; } //獲取權限失敗,會拋出異常 UsernamePasswordAuthenticationToken authentication = getAuthentication(request); //獲取后,將Authentication寫入SecurityContextHolder中供后序使用 SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } catch (Exception e) { responseJson(response); e.printStackTrace(); } } /** * 未登錄時的提示 * @param response */ private void responseJson(HttpServletResponse response){ try { //未登錄時,使用json進行提示 response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); PrintWriter out = response.getWriter(); Map<String,Object> map = new HashMap<String,Object>(); map.put("code",HttpServletResponse.SC_FORBIDDEN); map.put("message","請登錄!"); out.write(new ObjectMapper().writeValueAsString(map)); out.flush(); out.close(); } catch (Exception e1) { e1.printStackTrace(); } } /** * 通過token,獲取用戶信息 * @param request * @return */ private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { String token = request.getHeader("Authorization"); if (token != null) { //通過token解析出用戶信息 String user = Jwts.parser() //簽名、密鑰 .setSigningKey("MyJwtSecret") .parseClaimsJws(token.replace("Bearer ", "")) .getBody() .getSubject(); //不為null,返回 if (user != null) { return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>()); } return null; } return null; } }
- 在登錄過濾器中接收參數的實體類,也可以直接接收,這一個類不是必須的
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; } }
B項目的配置
第一步,在pom中引入必須的架包
<!-- 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.9.0</version> </dependency>
第二步,增加SecurityConfig配置文件
import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import com.example.demo.filter.JWTAuthenticationFilter; /** * SpringSecurity的配置 * 參考網址:https://blog.csdn.net/sxdtzhaoxinguo/article/details/77965226 * @author 程就人生 * @date 2019年5月26日 */ @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http //關閉跨站請求防護 .cors().and().csrf().disable() //允許不登陸就可以訪問的方法,多個用逗號分隔 .authorizeRequests() //其他的需要授權后訪問 .anyRequest().authenticated() .and() //增加是否登陸過濾 .addFilter(new JWTAuthenticationFilter(authenticationManager())) // 前后端分離是無狀態的,所以暫時不用session,將登陸信息保存在token中。 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } }
第三步,在增加對方法是否登錄進行攔截的過濾器
import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; 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 com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Jwts; /** * 是否登陸驗證方法 * @author 程就人生 * @date 2019年5月26日 */ public class JWTAuthenticationFilter extends BasicAuthenticationFilter { public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } /** * 對請求進行過濾 */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { try { //請求體的頭中是否包含Authorization String header = request.getHeader("Authorization"); //Authorization中是否包含Bearer,有一個不包含時直接返回 if (header == null || !header.startsWith("Bearer ")) { chain.doFilter(request, response); responseJson(response); return; } //獲取權限失敗,會拋出異常 UsernamePasswordAuthenticationToken authentication = getAuthentication(request); //獲取后,將Authentication寫入SecurityContextHolder中供后序使用 SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } catch (Exception e) { responseJson(response); e.printStackTrace(); } } /** * 未登錄時的提示 * @param response */ private void responseJson(HttpServletResponse response){ try { //未登錄時,使用json進行提示 response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); PrintWriter out = response.getWriter(); Map<String,Object> map = new HashMap<String,Object>(); map.put("code",HttpServletResponse.SC_FORBIDDEN); map.put("message","請登錄!"); out.write(new ObjectMapper().writeValueAsString(map)); out.flush(); out.close(); } catch (Exception e1) { e1.printStackTrace(); } } /** * 通過token,獲取用戶信息 * @param request * @return */ private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { String token = request.getHeader("Authorization"); if (token != null) { //通過token解析出用戶信息 String user = Jwts.parser() //簽名鹽 .setSigningKey("MyJwtSecret") .parseClaimsJws(token.replace("Bearer ", "")) .getBody() .getSubject(); //不為null,返回 if (user != null) { return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>()); } return null; } return null; } }
從B項目的配置中,可以看出,B項目配置的太簡潔了,只需要攔截一下沒有登錄的請求,連登錄也都省了。
A和B項目中分別添加一個Controller,用於測試
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * 測試用例 * @author 程就人生 * @date 2019年5月26日 */ @RestController public class IndexController { @GetMapping("/index") public Object index(){ return "index"; } }
使用測試工具進行測試
第一步,測試A項目和B項目的index是否能訪問,結果都不能訪問,測試結果OK


第二步,通過登錄獲取token,登錄成功后,返回了JSON格式的提示,返回的token在頭部,點擊響應頭,獲取token


第三步,將token拷貝至A項目index的頭部,B項目index的頭部,測試結果ok,都可以訪問,也可以把token時間設置的短一些,測試一下token過期了,是否還能訪問。


最后,感覺一下Token的結構,去掉前面固定的Bearer ,后面的分成三個部分,中間用點隔開,這個就簡單了解下吧。
- Header(頭部)
- Payload(負載)
- Signature(簽名)
Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJmZW5nIiwiZXhwIjoxNTU4OTUxMjM5fQ.X7lOhHJljxnVcNEckYSX22rgTDN0ToRJLaPb_1dAoPzx6q_eN5B5iOxO2GXoNUllIfQG6SrdJhgYzKZPTMsDIg
Spring Security整合JWT,實現單點登錄的功能,到此就告一段落了,看起來是不是很簡單呢,那就動手試一試吧。
作者:程就人生
鏈接:https://www.jianshu.com/p/8bd4a6e27e7f
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。