前面整理過一篇 SpringBoot Security前后端分離洛姑,登錄退出等返回json數(shù)據(jù),也就是用Spring Security皮服,基于SpringBoot2.1.4 RELEASE前后端分離的情況下楞艾,實現(xiàn)了登陸登出的功能参咙,亮點就在于以JSON的形式接收返回參數(shù)。這個是針對單個后臺服務(wù)的硫眯, 登錄信息都存儲在SecurityContextHolder緩存里蕴侧。如果是兩個或兩個以上的應(yīng)用呢,那該怎么辦两入?Session是不能用了净宵,Cookie自然也不能用,畢竟它倆是一對的裹纳。
曾想過用OAuth2來解決這個問題择葡,但是OAuth2太復(fù)雜,首先理解概念就需要花費一些時間剃氧,而且里面的授權(quán)服務(wù)器敏储、資源服務(wù)器、客戶端等等讓人傻傻分不清朋鞍,還有四種授權(quán)模式已添,要反復(fù)衡量,到底要用哪一種滥酥,概念還沒有扯清楚就開始糾結(jié)使用哪一個了酝碳。從概念入手不是個好主意,也不是個輕松的主意恨狈。在理解OAuth2的過程中疏哗,想到自己的項目是前后端分離的,離不開JSON禾怠,無意中遇見JWT返奉。JWT是什么玩意,咦吗氏,難道是自己苦苦尋求的嗎芽偏?!
那么弦讽,什么是JWT呢污尉?看看專家介紹 阮一峰的網(wǎng)絡(luò)日志,才知道往产,JWT 是JSON Web Token的簡稱被碗,它解決的就是跨域問題》麓澹看來锐朴,要找的就是它,簡單的蔼囊,但也是管用的焚志。
繼續(xù)深究衣迷,JWT到底是怎樣和SpringSecurity結(jié)合的呢。下面上代碼酱酬,在上代碼前先說明一下壶谒,在本次實例中,涉及到兩個項目膳沽,一個項目是登錄的項目A佃迄,另一個項目是根據(jù)token進(jìn)行訪問的項目B。其中B項目沒有登錄贵少,也不會涉及登錄呵俏,只要有Token就可以訪問,Token失效了就訪問不了了滔灶。
A項目是登錄的項目普碎,也是一個只能通過登錄進(jìn)行訪問的后臺服務(wù)。B項目就是一個服務(wù)录平,只要用戶在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的配置
* 參考網(wǎng)址: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
//關(guān)閉跨站請求防護(hù)
.cors().and().csrf().disable()
//允許不登陸就可以訪問的方法表箭,多個用逗號分隔
.authorizeRequests().antMatchers("/test").permitAll()
//其他的需要授權(quán)后訪問
.anyRequest().authenticated()
.and()
//增加登錄攔截
.addFilter(new JWTLoginFilter(authenticationManager()))
//增加是否登陸過濾
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
// 前后端分離是無狀態(tài)的赁咙,所以暫時不用session,將登陸信息保存在token中免钻。
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
//覆蓋UserDetailsService類
auth.userDetailsService(myCustomUserService)
//覆蓋默認(rèn)的密碼驗證類
.passwordEncoder(myPasswordEncoder);
}
}
第三步彼水,實現(xiàn)配置文件中自定義的類
- MyPasswordEncoder類實現(xiàn)了默認(rèn)的PasswordEncoder 接口,可以對密碼加密和密碼對比進(jìn)行個性化定制
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* 自定義的密碼加密方法极舔,實現(xiàn)了PasswordEncoder接口
* @author 程就人生
* @date 2019年5月26日
*/
@Component
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
//加密方法可以根據(jù)自己的需要修改
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return encode(charSequence).equals(s);
}
}
- MyCustomUserService 實現(xiàn)了框架默認(rèn)的UserDetailsService凤覆,可以根據(jù)username從數(shù)據(jù)庫獲取用戶,查看用戶是否存在
/**
* 登錄專用類,用戶登陸時拆魏,通過這里查詢數(shù)據(jù)庫
* 自定義類盯桦,實現(xiàn)了UserDetailsService接口,用戶登錄時調(diào)用的第一類
* @author 程就人生
* @date 2019年5月26日
*/
@Component
public class MyCustomUserService implements UserDetailsService {
/**
* 登陸驗證時渤刃,通過username獲取用戶的所有權(quán)限信息
* 并返回UserDetails放到spring的全局緩存SecurityContextHolder中拥峦,以供授權(quán)器使用
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//在這里可以自己調(diào)用數(shù)據(jù)庫,對username進(jìn)行查詢溪掀,看看在數(shù)據(jù)庫中是否存在
MyUserDetails myUserDetail = new MyUserDetails();
myUserDetail.setUsername(username);
myUserDetail.setPassword("123456");
return myUserDetail;
}
}
- MyUserDetails 實現(xiàn)了框架的UserDetails接口事镣,可以在該類中根據(jù)需要添加自己必需的屬性
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
/**
* 實現(xiàn)了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 實現(xiàn)了框架自帶的UsernamePasswordAuthenticationFilter 接口璃哟,對攔截做處理,以便登錄成功后喊递,在頭部設(shè)置token返回随闪;不管登錄成功還是失敗,都有JSON數(shù)據(jù)返回
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;
}
/**
* 接收并解析用戶憑證俏讹,出現(xiàn)錯誤時当宴,返回json數(shù)據(jù)前端
*/
@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 {
//未登錄出現(xiàn)賬號或密碼錯誤時,使用json進(jìn)行提示
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數(shù)據(jù)給前端
*/
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res,FilterChain chain, Authentication auth){
//json web token構(gòu)建
String token = Jwts.builder()
//此處為自定義的、實現(xiàn)org.springframework.security.core.userdetails.UserDetails的類殉疼,需要和配置中設(shè)置的保持一致
//此處的subject可以用一個用戶名梯浪,也可以是多個信息的組合,根據(jù)需要來定
.setSubject(((MyUserDetails) auth.getPrincipal()).getUsername())
//設(shè)置token過期時間瓢娜,24小時
.setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 24 * 1000))
//設(shè)置token簽名挂洛、密鑰
.signWith(SignatureAlgorithm.HS512, "MyJwtSecret")
.compact();
//返回token
res.addHeader("Authorization", "Bearer " + token);
try {
//登錄成功時,返回json格式進(jìn)行提示
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 類實現(xiàn)了BasicAuthenticationFilter 接口虏劲,對Controller中需要登錄后才能訪問的方法進(jìn)行了攔截,沒有登錄褒颈,則不能訪問伙单,返回JSON信息進(jìn)行提示
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);
}
/**
* 對請求進(jìn)行過濾
*/
@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;
}
//獲取權(quán)限失敗哈肖,會拋出異常
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進(jìn)行提示
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;
}
}
- 在登錄過濾器中接收參數(shù)的實體類游两,也可以直接接收,這一個類不是必須的
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的配置
* 參考網(wǎng)址: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
//關(guān)閉跨站請求防護(hù)
.cors().and().csrf().disable()
//允許不登陸就可以訪問的方法,多個用逗號分隔
.authorizeRequests()
//其他的需要授權(quán)后訪問
.anyRequest().authenticated()
.and()
//增加是否登陸過濾
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
// 前后端分離是無狀態(tài)的止吐,所以暫時不用session宝踪,將登陸信息保存在token中侨糟。
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
第三步,在增加對方法是否登錄進(jìn)行攔截的過濾器
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);
}
/**
* 對請求進(jìn)行過濾
*/
@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;
}
//獲取權(quán)限失敗秕重,會拋出異常
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進(jìn)行提示
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";
}
}
使用測試工具進(jìn)行測試
第一步聚蝶,測試A項目和B項目的index是否能訪問,結(jié)果都不能訪問藻治,測試結(jié)果OK
第二步碘勉,通過登錄獲取token,登錄成功后桩卵,返回了JSON格式的提示验靡,返回的token在頭部,點擊響應(yīng)頭雏节,獲取token
第三步胜嗓,將token拷貝至A項目index的頭部,B項目index的頭部钩乍,測試結(jié)果ok辞州,都可以訪問,也可以把token時間設(shè)置的短一些寥粹,測試一下token過期了变过,是否還能訪問。
最后涝涤,感覺一下Token的結(jié)構(gòu)媚狰,去掉前面固定的Bearer ,后面的分成三個部分阔拳,中間用點隔開崭孤,這個就簡單了解下吧。
- Header(頭部)
- Payload(負(fù)載)
- Signature(簽名)
Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJmZW5nIiwiZXhwIjoxNTU4OTUxMjM5fQ.X7lOhHJljxnVcNEckYSX22rgTDN0ToRJLaPb_1dAoPzx6q_eN5B5iOxO2GXoNUllIfQG6SrdJhgYzKZPTMsDIg
Spring Security整合JWT,實現(xiàn)單點登錄的功能辨宠,到此就告一段落了遗锣,看起來是不是很簡單呢,那就動手試一試吧彭羹。