前言
本章是基於上一章“JWT+SpringSecurity實現基於Token的單點登錄(一):前期準備”的基礎上進行開發的,如果前期準備還沒有做好的,可點擊鏈接至上一章。
代碼地址:gitee
一、JWT工具類
這裏我們使用jjwt來構建我們的Token。首先導入jjwt的依賴包。
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
接着構建JwtTokenUtils工具類。
package com.shiep.jwtauth.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
/**
* @author: 倪明輝
* @date: 2019/3/6 15:11
* @description: JWT工具類
* JWT是由三段組成的,分別是header(頭)、payload(負載)和signature(簽名)
* 其中header中放{
* "alg": "HS512",
* "typ": "JWT"
* } 表明使用的加密算法,和token的類型==>默認是JWT
*
*/
public class JwtTokenUtils {
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
//密鑰,用於signature(簽名)部分解密
private static final String PRIMARY_KEY = "jwtsecretdemo";
//簽發者
private static final String ISS = "Gent.Ni";
// 添加角色的key
private static final String ROLE_CLAIMS = "role";
// 過期時間是3600秒,既是1個小時
private static final long EXPIRATION = 3600L;
// 選擇了記住我之後的過期時間爲7天
private static final long EXPIRATION_REMEMBER = 604800L;
/**
* description: 創建Token
*
* @param username
* @param isRememberMe
* @return java.lang.String
*/
public static String createToken(String username, List<String> roles, boolean isRememberMe) {
long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
HashMap<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, roles);
return Jwts.builder()
//採用HS512算法對JWT進行的簽名,PRIMARY_KEY是我們的密鑰
.signWith(SignatureAlgorithm.HS512, PRIMARY_KEY)
//設置角色名
.setClaims(map)
//設置發證人
.setIssuer(ISS)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.compact();
}
/**
* description: 從token中獲取用戶名
*
* @param token
* @return java.lang.String
*/
public static String getUsername(String token){
return getTokenBody(token).getSubject();
}
// 獲取用戶角色
public static List<String> getUserRole(String token){
return (List<String>) getTokenBody(token).get(ROLE_CLAIMS);
}
/**
* description: 判斷Token是否過期
*
* @param token
* @return boolean
*/
public static boolean isExpiration(String token){
return getTokenBody(token).getExpiration().before(new Date());
}
/**
* description: 獲取
*
* @param token
* @return io.jsonwebtoken.Claims
*/
private static Claims getTokenBody(String token){
return Jwts.parser()
.setSigningKey(PRIMARY_KEY)
.parseClaimsJws(token)
.getBody();
}
}
JwtTokenUtils類的createToken方法,傳入參數UserName爲用戶名,roles是該用戶的角色列表,isRememberMe代表是否記住我,從而選擇Token的過期時間。getUsername和getUserRole方法分別用來讀取Token中的用戶名和該用戶的角色列表。isExpiration方法用來判斷該Token是否過期。
二、實現UserDetails,封裝用戶信息
package com.shiep.jwtauth.entity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
/**
* @author: 倪明輝
* @date: 2019/3/6 15:27
* @description: 實現UserDetails,封裝用戶信息,用於驗證身份
*/
public class JwtAuthUser implements UserDetails {
private Integer id;
private String userName;
private String password;
private List<String> roles;
private Collection<? extends GrantedAuthority> authorities;
/**
* description: 通過FXUser來創建JwtAuthUser
*
* @param user
* @return
*/
public JwtAuthUser(FXUser user){
this.id=user.getId();
this.userName=user.getName();
this.password=user.getPassword();
this.roles=user.getRoles();
}
/**
* description: 鑑權最重要方法,通過此方法來返回用戶權限
*
* @param
* @return java.util.Collection<? extends org.springframework.security.core.GrantedAuthority>
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new HashSet<>();
if (roles!=null) {
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
}
System.out.println("authorities:"+authorities);
return 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;
}
@Override
public String toString() {
return "JwtAuthUser{" +
"id=" + id +
", username='" + userName + '\'' +
", password='" + password + '\'' +
", authorities=" + roles +
'}';
}
}
JwtAuthUser類實現了UserDetails,從而封裝了用戶信息,用於認證和鑑權。
三、實現UserDetailsService,從數據庫加載用戶信息(UserDetails)
package com.shiep.jwtauth.service.impl;
import com.shiep.jwtauth.entity.FXUser;
import com.shiep.jwtauth.entity.JwtAuthUser;
import com.shiep.jwtauth.repository.FXUserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* @author: 倪明輝
* @date: 2019/3/6 16:26
* @description: 實現UserDetailsService,從數據庫中加載用戶信息==》用戶名、密碼及角色名
*
* Spring Security中進行身份驗證的是AuthenticationManager接口,ProviderManager是它的一個默認實現,但它並不用來處理身份認證,
* 而是委託給配置好的AuthenticationProvider,每個AuthenticationProvider會輪流檢查身份認證。檢查後或者返回Authentication對象或者拋出異常。
*
* 驗證身份就是加載響應的UserDetails,看看是否和用戶輸入的賬號、密碼、權限等信息匹配。
* 此步驟由實現AuthenticationProvider的DaoAuthenticationProvider(它利用UserDetailsService驗證用戶名、密碼和授權)處理。
* 包含 GrantedAuthority 的 UserDetails對象在構建 Authentication對象時填入數據。
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private FXUserRepository userRepository;
/**
* description: 通過用戶名從數據庫中讀取該用戶賬戶信息及權限信息
*
* @param userName 用戶名
* @return org.springframework.security.core.userdetails.UserDetails
*/
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
FXUser user = userRepository.findByName(userName);
if(user==null){
// 實際當用戶不存在時,應該頁面顯示錯誤信息,並跳轉到登錄界面
throw new UsernameNotFoundException("該用戶不存在!");
}
user.setRoles(userRepository.getRolesByUserName(userName));
System.out.println("UserDetailsServiceImpl==>loadUserByUsername:"+user.toString());
return new JwtAuthUser(user);
}
}
UserDetailsServiceImpl實現了UserDetailsService,只有一個方法loadUserByUsername,通過查詢用戶名從數據庫中加載用戶信息(UserDetails),這裏是JwtAuthUser。
爲了更清晰理解SpringSecurity的認證鑑權原理,下面講解下SpringSecurity中一些核心類。
- AuthenticationManager, 用戶認證的管理類,所有的認證請求(比如login)都會通過提交一個token給
AuthenticationManager
的authenticate()
方法來實現。當然事情肯定不是它來做,具體校驗動作會由AuthenticationManager
將請求轉發給具體的實現類來做。根據實現反饋的結果再調用具體的Handler來給用戶以反饋。 - AuthenticationProvider, 認證的具體實現類,一個provider是一種認證方式的實現,比如提交的用戶名密碼我是通過和DB中查出的user記錄做比對實現的,那就有一個
DaoProvider
;如果我是通過CAS請求單點登錄系統實現,那就有一個CASProvider
。
前面講了AuthenticationManager
只是一個代理接口,真正的認證就是由AuthenticationProvider
來做的。一個AuthenticationManager
可以包含多個Provider,每個provider通過實現一個support方法來表示自己支持那種Token的認證。AuthenticationManager
默認的實現類是ProviderManager
。 - UserDetailService, 用戶認證通過Provider來做,所以Provider需要拿到系統已經保存的認證信息,獲取用戶信息的接口spring-security抽象成
UserDetailService
。 - AuthenticationToken, 所有提交給
AuthenticationManager
的認證請求都會被封裝成一個Token的實現,比如最容易理解的UsernamePasswordAuthenticationToken
。 - SecurityContext,當用戶通過認證之後,就會爲這個用戶生成一個唯一的
SecurityContext
,裏面包含用戶的認證信息Authentication
。通過SecurityContext我們可以獲取到用戶的標識Principle
和授權信息GrantedAuthrity
。在系統的任何地方只要通過SecurityHolder.getSecruityContext()
就可以獲取到SecurityContext
。
這裏先大概瞭解下,下面我們接着Coding。
四、配置登錄校驗攔截器
在配置登錄校驗攔截器前,我們一般先創建一個Model類,用來接收用戶登錄信息。
package com.shiep.jwtauth.model;
import lombok.Data;
/**
* @author: 倪明輝
* @date: 2019/3/6 16:18
* @description: 封裝了用戶登錄時的信息
*/
@Data
public class LoginUser {
private String username;
private String password;
private Boolean rememberMe;
}
LoginUser類中有三個字段,用戶名、密碼、是否記住我。實際開發時還能加入驗證碼等。
配置好了LoginUser類後,我們接着來看看如何配置過濾器。
package com.shiep.jwtauth.filter;
import com.alibaba.fastjson.JSON;
import com.shiep.jwtauth.common.ResultEnum;
import com.shiep.jwtauth.common.ResultVO;
import com.shiep.jwtauth.entity.JwtAuthUser;
import com.shiep.jwtauth.model.LoginUser;
import com.shiep.jwtauth.utils.JwtTokenUtils;
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.core.GrantedAuthority;
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.Collection;
import java.util.List;
/**
* @author: 倪明輝
* @date: 2019/3/6 16:12
* @description: 進行用戶賬號的驗證==>認證功能
*
*/
public class JwtLoginAuthFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private ThreadLocal<Boolean> rememberMe = new ThreadLocal<>();
public JwtLoginAuthFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
// 設置該過濾器地址
super.setFilterProcessesUrl("/auth/login");
}
/**
* description: 登錄驗證
*
* @param request
* @param response
* @return org.springframework.security.core.Authentication
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
LoginUser loginUser = new LoginUser();
loginUser.setUsername(request.getParameter("username"));
loginUser.setPassword(request.getParameter("password"));
loginUser.setRememberMe(Boolean.parseBoolean(request.getParameter("rememberMe")));
System.out.println(loginUser.toString());
rememberMe.set(loginUser.getRememberMe());
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>())
);
}
/**
* description: 登錄驗證成功後調用,驗證成功後將生成Token,並重定向到用戶主頁home
* 與AuthenticationSuccessHandler作用相同
*
* @param request
* @param response
* @param chain
* @param authResult
* @return void
*/
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 查看源代碼會發現調用getPrincipal()方法會返回一個實現了`UserDetails`接口的對象,這裏是JwtAuthUser
JwtAuthUser jwtUser = (JwtAuthUser) authResult.getPrincipal();
System.out.println("JwtAuthUser:" + jwtUser.toString());
boolean isRemember = rememberMe.get();
List<String> roles = new ArrayList<>();
Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
for (GrantedAuthority authority : authorities){
roles.add(authority.getAuthority());
}
System.out.println("roles:"+roles);
String token = JwtTokenUtils.createToken(jwtUser.getUsername(), roles,isRemember);
System.out.println("token:"+token);
// 重定向無法設置header,這裏設置header只能設置到/auth/login界面的header
//response.setHeader("token", JwtTokenUtils.TOKEN_PREFIX + token);
// 登錄成功重定向到home界面
// 這裏先採用參數傳遞
response.sendRedirect("/home?token="+token);
}
/**
* description: 登錄驗證失敗後調用,這裏直接Json返回,實際上可以重定向到錯誤界面等
* 與AuthenticationFailureHandler作用相同
*
* @param request
* @param response
* @param failed
* @return void
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(JSON.toJSONString(ResultVO.result(ResultEnum.USER_LOGIN_FAILED,false)));
}
}
JwtLoginAuthFilter實現了UsernamePasswordAuthenticationFilter,該攔截器的attemptAuthentication方法,通過request.getParameter方法來得到前端傳來的登錄參數,並構建LoginUser對象。PS:從代碼看LoginUser是不是有點多餘?我們是不是可以直接得到參數進行校驗?嗯……是可以
的,但是原本正確的思路是通過從輸入流中直接構建登錄對象的,代碼如下:
LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);
但是LoginUser的Boolean rememberMe字段,老是無法與前端數據進行匹對(將Boolean改成String也報錯,錯誤信息如下),因此這裏先採用這種方式讀取數據吧。ps:有知道如何解決的小夥伴,麻煩告訴我下,感激不盡~
com.fasterxml.jackson.databind.exc.MismatchedInputException: No content to map due to end-of-input
我們接着看attemptAuthentication方法,該方法從登錄界面讀取到數據後,通過authenticationManager.authenticate方法,讓SpringSecurity去進行驗證,不需要自己查數據庫對用戶名和密碼進行配對。
attemptAuthentication方法進行校驗後,有兩種結果:成功或失敗。當驗證成功時將調用successfulAuthentication方法,失敗調用unsuccessfulAuthentication方法。
successfulAuthentication方法中首先通過Authentication.getPrincipal()方法來得到當前用戶的信息(UserDetails),接着通過用戶名、角色列表和是否記住我來構建Token。原本應該Token將放在response的header中的,但是設置header只能設置在當前頁面的response中?(有知道如何設置重定向後頁面的header的小夥伴,麻煩告訴我下,感激不盡~(/ □ \))至於爲什麼要頁面重定向先賣個關子,我們先往下看。
unsuccessfulAuthentication方法是用戶認證失敗後返回信息,這裏用到了兩個工具類,下面我們先來看看他們。
五、response狀態碼
package com.shiep.jwtauth.common;
import lombok.Getter;
/**
* @author: 倪明輝
* @date: 2019/3/7 17:13
* @description: JWT認證==》認證結果的枚舉類
*/
@Getter
public enum ResultEnum {
/**
* description: 認證結果狀態碼及信息
*
* @param null
* @return
*/
SUCCESS(101,"成功"),
FAILURE(102,"失敗"),
USER_NEED_AUTHORITIES(201,"用戶未登錄"),
USER_LOGIN_FAILED(202,"用戶賬號或密碼錯誤"),
USER_LOGIN_SUCCESS(203,"用戶登錄成功"),
USER_NO_ACCESS(204,"用戶無權訪問"),
USER_LOGOUT_SUCCESS(205,"用戶登出成功"),
TOKEN_IS_BLACKLIST(206,"此token爲黑名單"),
LOGIN_IS_OVERDUE(207,"登錄已失效"),
;
private Integer code;
private String message;
ResultEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
/**
* description: 通過code返回message
*
* @param code
* @return com.shiep.jwtauth.common.ResultEnum
*/
public static ResultEnum parse(int code){
ResultEnum[] values = values();
for (ResultEnum value : values) {
if(value.getCode() == code){
return value;
}
}
throw new RuntimeException("Unknown code of ResultEnum");
}
}
ResultEnum封裝一些常用的狀態碼。
package com.shiep.jwtauth.common;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* @author: 倪明輝
* @date: 2019/3/7 17:23
* @description: response返回結果集
*/
public class ResultVO implements Serializable {
private static final long serialVersionUID = -5359028332240046810L;
/**
* description: 返回響應信息
*
* @param respCode
* @param success
* @return java.util.Map<java.lang.String,java.lang.Object>
*/
public static Map<String, Object> result(ResultEnum respCode, Boolean success) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("code", respCode.getCode());
map.put("message", respCode.getMessage());
map.put("data", null);
map.put("success",success);
return map;
}
/**
* description: 返回響應信息及Token
*
* @param respCode
* @param jwtToken
* @param success
* @return java.util.Map<java.lang.String,java.lang.Object>
*/
public final static Map<String, Object> result(ResultEnum respCode, String jwtToken, Boolean success) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("jwtToken",jwtToken);
map.put("code", respCode.getCode());
map.put("message", respCode.getMessage());
map.put("data", null);
map.put("success",success);
return map;
}
}
ResultVO用於返回結果集。
六、BasicAuthenticationFilter過濾器
package com.shiep.jwtauth.filter;
import com.shiep.jwtauth.utils.JwtTokenUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.Collection;
import java.util.HashSet;
import java.util.List;
/**
* @author: 倪明輝
* @date: 2019/3/6 16:20
* @description: 對所有請求進行過濾
* BasicAuthenticationFilter繼承於OncePerRequestFilter==》確保在一次請求只通過一次filter,而不需要重複執行。
*/
public class JwtPreAuthFilter extends BasicAuthenticationFilter {
public JwtPreAuthFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
/**
* description: 從request的header部分讀取Token
*
* @param request
* @param response
* @param chain
* @return void
*/
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
System.out.println("BasicAuthenticationFilters");
String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
System.out.println("tokenHeader:"+tokenHeader);
// 如果請求頭中沒有Authorization信息則直接放行了
if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 如果請求頭中有token,則進行解析,並且設置認證信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
super.doFilterInternal(request, response, chain);
}
/**
* description: 讀取Token信息,創建UsernamePasswordAuthenticationToken對象
*
* @param tokenHeader
* @return org.springframework.security.authentication.UsernamePasswordAuthenticationToken
*/
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
//解析Token時將“Bearer ”前綴去掉
String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
String username = JwtTokenUtils.getUsername(token);
List<String> roles = JwtTokenUtils.getUserRole(token);
Collection<GrantedAuthority> authorities = new HashSet<>();
if (roles!=null) {
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
}
if (username != null){
return new UsernamePasswordAuthenticationToken(username, null, authorities);
}
return null;
}
}
在JwtLoginAuthFilter中我們對登錄的用戶進行認證,認證成功時將生成Token給用戶前端,之後前端保存該Token在Cookie或session中,當用戶訪問服務器時,只需攜帶該Token即可訪問服務。而Token的認證鑑權工作就是由本例中的JwtPreAuthFilter來實現了。
JwtPreAuthFilter繼承了BasicAuthenticationFilter,該過濾器是繼承於OncePerRequestFilter,用於確保在一次請求只通過一次filter,而不需要重複執行。簡單的說,就是用戶的每次請求都將經過該過濾器。下面我們詳細看看該過濾器中的代碼。
doFilterInternal方法從request的header中查看是否帶有Token,如果沒有則放行,如果有則進行Token解析(調用getAuthentication方法),並設置認證信息。
七、配置Handler
package com.shiep.jwtauth.handler;
import com.alibaba.fastjson.JSON;
import com.shiep.jwtauth.common.ResultEnum;
import com.shiep.jwtauth.common.ResultVO;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author: 倪明輝
* @date: 2019/3/8 9:44
* @description: 用戶登出成功時返回給前端的數據
*/
public class FxLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.getWriter().write(JSON.toJSONString(ResultVO.result(ResultEnum.USER_LOGOUT_SUCCESS,true)));
}
}
package com.shiep.jwtauth.handler;
import com.alibaba.fastjson.JSON;
import com.shiep.jwtauth.common.ResultEnum;
import com.shiep.jwtauth.common.ResultVO;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author: 倪明輝
* @date: 2019/3/7 15:18
* @description: 用戶未登錄時返回給前端的數據
*/
public class UnAuthorizedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
// response.setStatus(HttpServletResponse.SC_FORBIDDEN);
// String reason = "統一處理,原因:"+authException.getMessage();
// response.getWriter().write(new ObjectMapper().writeValueAsString(reason));
response.getWriter().write(JSON.toJSONString(ResultVO.result(ResultEnum.USER_NEED_AUTHORITIES,false)));
}
}
FxLogoutSuccessHandler是用戶成功登出時調用的,而UnAuthorizedEntryPoint是用戶未登錄時調用的。Spring中handle還有許多,不一一列舉了。
八、配置SpringSecurity
到這裏基本操作都寫好啦,現在就需要我們將這些辛苦寫好的“組件”組合到一起發揮作用了,那就需要配置SpringSecurity了。
package com.shiep.jwtauth.config;
import com.shiep.jwtauth.filter.JwtLoginAuthFilter;
import com.shiep.jwtauth.filter.JwtPreAuthFilter;
import com.shiep.jwtauth.handler.FxLogoutSuccessHandler;
import com.shiep.jwtauth.handler.UnAuthorizedEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
/**
* @author: 倪明輝
* @date: 2019/3/6 16:24
* @description:
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
// 因爲UserDetailsService的實現類實在太多啦,這裏設置一下我們要注入的實現類
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
// 加密器
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* description: 加載userDetailsService,用於從數據庫中取用戶信息
*
* @param auth
* @return void
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
/**
* description: http細節
*
* @param http
* @return void
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 開啓跨域資源共享
http.cors()
.and()
// 關閉csrf
.csrf().disable()
// 關閉session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic().authenticationEntryPoint(new UnAuthorizedEntryPoint())
.and()
.authorizeRequests()
// 需要角色爲ADMIN才能刪除該資源
.antMatchers(HttpMethod.DELETE,"/tasks/**").hasAnyRole("ADMIN")
// 測試用資源,需要驗證了的用戶才能訪問
.antMatchers("/tasks/**").authenticated()
// 其他都放行了
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
//.successHandler(new Fx)
.and()
.logout()//默認註銷行爲爲logout
.logoutSuccessHandler(new FxLogoutSuccessHandler())
.and()
// 添加到過濾鏈中
// 先是UsernamePasswordAuthenticationFilter用於login校驗
.addFilter(new JwtLoginAuthFilter(authenticationManager()))
// 再通過OncePerRequestFilter,對其他請求過濾
.addFilter(new JwtPreAuthFilter(authenticationManager()));
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
configure(HttpSecurity http)方法是重點,具體在代碼註釋裏面已經解釋清楚了。下面我們來配置下Controller和視圖。
注意:配置SpringSecurity,需要從細粒度到粗粒度。不然細粒度將不起作用。
九、thymeleaf視圖
loginPage.html==》登錄界面
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
<script>
function changeValue(){
var check = document.getElementById("rememberMe");
if(check.checked == true){
document.getElementById("rememberMe").value = true;
}else{
document.getElementById("rememberMe").value = false;
}
}
</script>
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/auth/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><label> RememberMe: <input type="checkbox" name="rememberMe" id="rememberMe" onclick="changeValue()"/></label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
loginPage頁面,定義了username、password和rememberMe來得到用戶登錄信息(看前面的登錄認證過濾器),接着提交到“/auth/login”路徑,該路徑就是前面JwtLoginAuthFilter中配置的路徑,提交到該過濾器中進行登錄驗證。
homePage.html==》用戶登錄驗證成功後進入的用戶主頁
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Home</title>
<script src="jquery-3.1.1.min.js" type="text/javascript"></script>
<script src="getToken.js" type="text/javascript"></script>
</head>
<body>
<script>
var token=$.getUrlParam("token");
window.onload = function () {
$("#token").val(token);
}
function deleteTask() {
$.ajax({
type : 'delete',
url : '/tasks/1',
contentType : 'application/json;charset=UTF-8',
dataType : 'text',
beforeSend: function(request) {
request.setRequestHeader("Authorization","Bearer "+token);
},
success : function(data,textStatus,jqXHR){
alert("response:"+data);
},
error: function (err) {
alert("ajax錯誤碼:" + err.status);
}
});
}
</script>
<h1>Login success</h1>
<textarea id="token" rows="5" cols="50"></textarea>
<input type="button" value="admin角色才能刪除" onclick="deleteTask()"/>
</body>
</html>
homePage.html中導入了jquery和自己寫的一個getToken.js,用於從URL中得到參數。而按鈕的點擊事件調用deleteTask()方法,用於發送delete請求到“/tasks”,該路徑需要“ROLE_ADMIN”權限。
/*獲取到Url裏面的參數*/
(function ($) {
$.getUrlParam = function (name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]); return null;
}
})(jQuery);
十、Controller控制層
package com.shiep.jwtauth.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @author: 倪明輝
* @date: 2019/3/8 10:31
* @description:
*/
@Controller
public class LoginController {
@GetMapping("/login")
public String toLoginPage(){
return "loginPage";
}
@GetMapping("/home")
public String toHomePage(){
return "homePage";
}
}
LoginController中定義了“/login”路徑指向loginPage.html頁面,“/home”路徑指向homePage.html頁面。
package com.shiep.jwtauth.controller;
import org.springframework.web.bind.annotation.*;
/**
* @author: 倪明輝
* @date: 2019/3/6 16:35
* @description:
*/
@RestController
@RequestMapping(path = "/tasks",produces = "application/json;charset=gbk")
public class TaskController {
@GetMapping
public String listTasks(){
return "任務列表";
}
@PostMapping
public String newTasks(){
return "創建了一個新的任務";
}
@PutMapping("/{taskId}")
public String updateTasks(@PathVariable("taskId")Integer id){
return "更新了一下id爲:"+id+"的任務";
}
@DeleteMapping("/{taskId}")
public String deleteTasks(@PathVariable("taskId")Integer id){
return "刪除了id爲:"+id+"的任務";
}
}
TaskController就是對應SpringSecurity配置類中的需要權限才能訪問的路徑。
好了,終於開發完成了,接下來我們運行項目來測試下吧。
十一、運行結果測試
首先,我們在數據庫中插入兩個用戶,通過上一章的方法進行插入用戶。
用戶名:wang,密碼:123(數據庫中的密碼是經過加密後的),該用戶具有的權限(ROLE_USER、ROLE_ADMIN)
用戶名:li,密碼:123(數據庫中的密碼是經過加密後的),該用戶具有的權限(ROLE_USER)
接着使用瀏覽器訪問http://localhost:8080/tasks,即get方式。因爲“/tasks/**”路徑是需要權限認證的,但是我們此時未驗證(在header中設置Token),因此會跳轉到http://localhost:8080/login界面。
我們在這個界面使用wang用戶登錄。頁面從“/login”跳轉到“/auth/login”路徑進行認證,認證成功重定向到“/home”頁面,並通過參數形式攜帶Token到前臺。現在我們看看程序的控制檯輸出:
可以看到認證成功了,Token已經生成。下面是home界面。
當我們點擊按鈕時:
可以看到權限認證成功,已經執行方法。
接着,我們重新以li用戶登錄試試,步驟同上。
發現發生403錯誤,這是用戶無權限。
十二、後記
以上關於使用JWT+SpringSecurity實現基於Token的單點登錄之認證和授權部分已經完成。但是除了上面提到的,還有一些問題:因爲JWT的最大缺點是服務器不保存會話狀態,所以在使用期間不可能取消令牌或更改令牌的權限。也就是說,一旦JWT簽發,在有效期內將會一直有效。在實際情況下,用戶登出時,應該使本次的Token失效。這是其中一個問題。另外關於JWT,我們還能繼續跟Redis進行集成,將其緩存到Redis中。最後,跟SpringCloud進行繼承,作爲Zuul網關的認證入口。這些我們將在下一章繼續開發。