SpringBoot集成Spring Security用JWT令牌實(shí)現(xiàn)登錄和鑒權(quán)的方法
最近在做項(xiàng)目的過程中 需要用JWT做登錄和鑒權(quán) 查了很多資料 都不甚詳細(xì)
有的是需要在application.yml里進(jìn)行jwt的配置 但我在導(dǎo)包后并沒有相應(yīng)的配置項(xiàng) 因而并不適用
在踩過很多坑之后 稍微整理了一下 做個(gè)筆記
一、概念
1、什么是JWT
Json Web Token (JWT)是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標(biāo)準(zhǔn)(RFC 7519)
該token被設(shè)計(jì)為緊湊且安全的 特別適用于分布式站點(diǎn)的單點(diǎn)登錄(SSO)場(chǎng)景
隨著JWT的出現(xiàn) 使得校驗(yàn)方式更加簡(jiǎn)單便捷化
JWT實(shí)際上就是一個(gè)字符串 它由三部分組成:頭部 載荷和簽名
用[.
]分隔這三個(gè)部分 最終的格式類似于:xxxx.xxxx.xxxx
在服務(wù)器直接根據(jù)token取出保存的用戶信息 即可對(duì)token的可用性進(jìn)行校驗(yàn) 使得單點(diǎn)登錄更為簡(jiǎn)單
2、JWT校驗(yàn)的過程
1、瀏覽器發(fā)送用戶名和密碼 發(fā)起登錄請(qǐng)求
2、服務(wù)端驗(yàn)證身份 根據(jù)算法將用戶標(biāo)識(shí)符打包生成token字符串 并且返回給瀏覽器
3、當(dāng)瀏覽器需要發(fā)起請(qǐng)求時(shí) 將token一起發(fā)送給服務(wù)器
4、服務(wù)器發(fā)現(xiàn)數(shù)據(jù)中攜帶有token 隨即進(jìn)行解密和鑒權(quán)
5、校驗(yàn)成功 服務(wù)器返回請(qǐng)求的數(shù)據(jù)
二、使用
1、首先是導(dǎo)包
<!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Spring Security和JWT整合 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.10.RELEASE</version> </dependency> <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!-- 字符串轉(zhuǎn)換需要用到此包 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> </dependency>
2、實(shí)體類
兩個(gè)實(shí)體類 一個(gè)是用戶 另一個(gè)是權(quán)限
public class User { private Integer id; private String username; private String password; 省略gettersetter之類的代碼... }
public class Role { private Integer id; private String username; private String name; 省略gettersetter之類的代碼... }
3、然后需要一個(gè)Utils工具類
該類用于進(jìn)行Token的加密和解密 可在此類中單元測(cè)試
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; import java.util.HashMap; import java.util.Map; public class JwtTokenUtil { // Token請(qǐng)求頭 public static final String TOKEN_HEADER = "Authorization"; // Token前綴 public static final String TOKEN_PREFIX = "Bearer "; // 簽名主題 public static final String SUBJECT = "piconjo"; // 過期時(shí)間 public static final long EXPIRITION = 1000 * 24 * 60 * 60 * 7; // 應(yīng)用密鑰 public static final String APPSECRET_KEY = "piconjo_secret"; // 角色權(quán)限聲明 private static final String ROLE_CLAIMS = "role"; /** * 生成Token */ public static String createToken(String username,String role) { Map<String,Object> map = new HashMap<>(); map.put(ROLE_CLAIMS, role); String token = Jwts .builder() .setSubject(username) .setClaims(map) .claim("username",username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRITION)) .signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact(); return token; } /** * 校驗(yàn)Token */ public static Claims checkJWT(String token) { try { final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims; } catch (Exception e) { e.printStackTrace(); return null; } } /** * 從Token中獲取username */ public static String getUsername(String token){ Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims.get("username").toString(); } /** * 從Token中獲取用戶角色 */ public static String getUserRole(String token){ Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims.get("role").toString(); } /** * 校驗(yàn)Token是否過期 */ public static boolean isExpiration(String token){ Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims.getExpiration().before(new Date()); } }
4、配置UserDetailsService的實(shí)現(xiàn)類 用于加載用戶信息
import xxx.xxx.xxx.bean.Role; // 自己的包 import xxx.xxx.xxx.bean.User; // 自己的包 import xxx.xxx.xxx.mapper.UserMapper; // 自己的包 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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; import java.util.ArrayList; import java.util.List; @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { if (s == null || "".equals(s)) { throw new RuntimeException("用戶不能為空"); } // 調(diào)用方法查詢用戶 User user = userMapper.findUserByUsername(s); if (user == null) { throw new RuntimeException("用戶不存在"); } List<SimpleGrantedAuthority> authorities = new ArrayList<>(); for (Role role:userMapper.findRoleByUsername(s)) { authorities.add(new SimpleGrantedAuthority("ROLE_"+role.getName())); } return new org.springframework.security.core.userdetails.User(user.getUsername(),"{noop}"+user.getPassword(),authorities); } }
5、然后 配置兩個(gè)攔截器
其中 一個(gè)用于登錄 另一個(gè)用于鑒權(quán)
JWTAuthenticationFilter登錄攔截器:
該攔截器用于獲取用戶登錄的信息
至于具體的驗(yàn)證 只需創(chuàng)建一個(gè)token并調(diào)用authenticationManager的authenticate()方法
讓Spring security驗(yàn)證即可 驗(yàn)證的事交給框架
import com.alibaba.fastjson.JSON; import xxx.xxx.xxx.utils.JwtTokenUtil; // 自己的包 import org.springframework.security.authentication.*; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User; 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.Collection; /** * 驗(yàn)證用戶名密碼正確后 生成一個(gè)token并將token返回給客戶端 */ public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } /** * 驗(yàn)證操作 接收并解析用戶憑證 */ @Override public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException { // 從輸入流中獲取到登錄的信息 // 創(chuàng)建一個(gè)token并調(diào)用authenticationManager.authenticate() 讓Spring security進(jìn)行驗(yàn)證 return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getParameter("username"),request.getParameter("password"))); } /** * 驗(yàn)證【成功】后調(diào)用的方法 * 若驗(yàn)證成功 生成token并返回 */ @Override protected void successfulAuthentication(HttpServletRequest request,HttpServletResponse response,FilterChain chain,Authentication authResult) throws IOException { User user= (User) authResult.getPrincipal(); // 從User中獲取權(quán)限信息 Collection<? extends GrantedAuthority> authorities = user.getAuthorities(); // 創(chuàng)建Token String token = JwtTokenUtil.createToken(user.getUsername(), authorities.toString()); // 設(shè)置編碼 防止亂碼問題 response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); // 在請(qǐng)求頭里返回創(chuàng)建成功的token // 設(shè)置請(qǐng)求頭為帶有"Bearer "前綴的token字符串 response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token); // 處理編碼方式 防止中文亂碼 response.setContentType("text/json;charset=utf-8"); // 將反饋塞到HttpServletResponse中返回給前臺(tái) response.getWriter().write(JSON.toJSONString("登錄成功")); } /** * 驗(yàn)證【失敗】調(diào)用的方法 */ @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { String returnData=""; // 賬號(hào)過期 if (failed instanceof AccountExpiredException) { returnData="賬號(hào)過期"; } // 密碼錯(cuò)誤 else if (failed instanceof BadCredentialsException) { returnData="密碼錯(cuò)誤"; } // 密碼過期 else if (failed instanceof CredentialsExpiredException) { returnData="密碼過期"; } // 賬號(hào)不可用 else if (failed instanceof DisabledException) { returnData="賬號(hào)不可用"; } //賬號(hào)鎖定 else if (failed instanceof LockedException) { returnData="賬號(hào)鎖定"; } // 用戶不存在 else if (failed instanceof InternalAuthenticationServiceException) { returnData="用戶不存在"; } // 其他錯(cuò)誤 else{ returnData="未知異常"; } // 處理編碼方式 防止中文亂碼 response.setContentType("text/json;charset=utf-8"); // 將反饋塞到HttpServletResponse中返回給前臺(tái) response.getWriter().write(JSON.toJSONString(returnData)); } }
JWTAuthorizationFilter權(quán)限校驗(yàn)攔截器:
當(dāng)訪問需要權(quán)限校驗(yàn)的URL(當(dāng)然 該URL也是需要經(jīng)過配置的) 則會(huì)來到此攔截器 在該攔截器中對(duì)傳來的Token進(jìn)行校驗(yàn)
只需告訴Spring security該用戶是否已登錄 并且是什么角色 擁有什么權(quán)限即可
import xxx.xxx.xxx.utils.JwtTokenUtil; // 自己的包 import org.apache.commons.lang3.StringUtils; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 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.ArrayList; import java.util.Collection; /** * 登錄成功后 走此類進(jìn)行鑒權(quán)操作 */ public class JWTAuthorizationFilter extends BasicAuthenticationFilter { public JWTAuthorizationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } /** * 在過濾之前和之后執(zhí)行的事件 */ @Override protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain) throws IOException, ServletException { String tokenHeader = request.getHeader(JwtTokenUtil.TOKEN_HEADER); // 若請(qǐng)求頭中沒有Authorization信息 或是Authorization不以Bearer開頭 則直接放行 if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX)) { chain.doFilter(request, response); return; } // 若請(qǐng)求頭中有token 則調(diào)用下面的方法進(jìn)行解析 并設(shè)置認(rèn)證信息 SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader)); super.doFilterInternal(request, response, chain); } /** * 從token中獲取用戶信息并新建一個(gè)token * * @param tokenHeader 字符串形式的Token請(qǐng)求頭 * @return 帶用戶名和密碼以及權(quán)限的Authentication */ private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) { // 去掉前綴 獲取Token字符串 String token = tokenHeader.replace(JwtTokenUtil.TOKEN_PREFIX, ""); // 從Token中解密獲取用戶名 String username = JwtTokenUtil.getUsername(token); // 從Token中解密獲取用戶角色 String role = JwtTokenUtil.getUserRole(token); // 將[ROLE_XXX,ROLE_YYY]格式的角色字符串轉(zhuǎn)換為數(shù)組 String[] roles = StringUtils.strip(role, "[]").split(", "); Collection<SimpleGrantedAuthority> authorities=new ArrayList<>(); for (String s:roles) { authorities.add(new SimpleGrantedAuthority(s)); } if (username != null) { return new UsernamePasswordAuthenticationToken(username, null,authorities); } return null; } }
6、再配置一個(gè)自定義類 用于進(jìn)行匿名用戶訪問資源時(shí)無權(quán)限的處理
該類需實(shí)現(xiàn)AuthenticationEntryPoint
import com.alibaba.fastjson.JSONObject; 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; public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setCharacterEncoding("utf-8"); response.setContentType("text/javascript;charset=utf-8"); response.getWriter().print(JSONObject.toJSONString("您未登錄,沒有訪問權(quán)限")); } }
7、最后 將這些組件組裝到一起即可
創(chuàng)建一個(gè)自定義的配置類 繼承WebSecurityConfigurerAdapter
在該類上 需加@EnableWebSecurity
注解 配置Web安全過濾器和啟用全局認(rèn)證機(jī)制
import xxx.xxx.xxx.JWTAuthenticationEntryPoint; // 自己的包 import xxx.xxx.xxx.xxx.JWTAuthenticationFilter; // 自己的包 import xxx.xxx.xxx.xxx.JWTAuthorizationFilter; // 自己的包 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; 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.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.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired @Qualifier("userDetailsServiceImpl") private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } /** * 安全配置 */ @Override protected void configure(HttpSecurity http) throws Exception { // 跨域共享 http.cors() .and() // 跨域偽造請(qǐng)求限制無效 .csrf().disable() .authorizeRequests() // 訪問/data需要ADMIN角色 .antMatchers("/data").hasRole("ADMIN") // 其余資源任何人都可訪問 .anyRequest().permitAll() .and() // 添加JWT登錄攔截器 .addFilter(new JWTAuthenticationFilter(authenticationManager())) // 添加JWT鑒權(quán)攔截器 .addFilter(new JWTAuthorizationFilter(authenticationManager())) .sessionManagement() // 設(shè)置Session的創(chuàng)建策略為:Spring Security永不創(chuàng)建HttpSession 不使用HttpSession來獲取SecurityContext .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() // 異常處理 .exceptionHandling() // 匿名用戶訪問無權(quán)限資源時(shí)的異常 .authenticationEntryPoint(new JWTAuthenticationEntryPoint()); } /** * 跨域配置 * @return 基于URL的跨域配置信息 */ @Bean CorsConfigurationSource corsConfigurationSource() { final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // 注冊(cè)跨域配置 source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues()); return source; } }
定義一個(gè)用于測(cè)試的對(duì)外映射接口:
@RestController public class UserController { @GetMapping("/data") private ResponseUtil data() { return "This is data."; } }
默認(rèn)登錄路徑是/login
用POST請(qǐng)求發(fā)送
若要修改默認(rèn)的登錄路徑 只需要在自己定義的登錄過濾器JWTAuthenticationFilter的構(gòu)造方法里進(jìn)行配置即可
比如 若想修改為/api/login:
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; // 設(shè)置登錄URL super.setFilterProcessesUrl("/api/login"); }
登錄時(shí) 參數(shù)的屬性名分別是username和password 不能改動(dòng):
登錄成功后會(huì)返回一個(gè)Token:
在請(qǐng)求需要權(quán)限的接口路徑時(shí) 若不帶上Token 則會(huì)提示沒有訪問權(quán)限
帶上Token后再次請(qǐng)求 即可正常訪問:
注:Token的前面要帶有Bearer
的前綴
這樣 一個(gè)基本的實(shí)現(xiàn)就差不多完成了
為簡(jiǎn)單演示 在該案例中就不對(duì)密碼進(jìn)行加密了 實(shí)際開發(fā)是需要對(duì)明文密碼加密后存儲(chǔ)的 推薦用BCrypt進(jìn)行加密和解密
為節(jié)省篇幅 用于注冊(cè)的接口也不寫了 實(shí)際上在注冊(cè)接口傳入的密碼也需要用BCrypt加密后再存入數(shù)據(jù)庫(kù)中
還可以用Redis進(jìn)行Token的存儲(chǔ) 這些都是后話了
到此這篇關(guān)于SpringBoot集成Spring Security用JWT令牌實(shí)現(xiàn)登錄和鑒權(quán)的方法的文章就介紹到這了,更多相關(guān)SpringBoot JWT令牌登錄和鑒權(quán)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
項(xiàng)目管理利器-Maven(Windows安裝)圖文教程
下面小編就為大家?guī)硪黄?xiàng)目管理利器-Maven(Windows安裝)圖文教程。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-06-06java線程之用Thread類創(chuàng)建線程的方法
本篇文章介紹了,Thread類創(chuàng)建線程的方法。需要的朋友參考下2013-05-05認(rèn)識(shí)Java底層操作系統(tǒng)與并發(fā)基礎(chǔ)
這篇文章主要介紹了認(rèn)識(shí)Java底層操作系統(tǒng)與并發(fā)基礎(chǔ),文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-07-07SpringBoot自定義注解實(shí)現(xiàn)Token校驗(yàn)的方法
這篇文章主要介紹了SpringBoot自定義注解實(shí)現(xiàn)Token校驗(yàn)的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03Spring里的Async注解實(shí)現(xiàn)異步操作的方法步驟
這篇文章主要介紹了Spring里的Async注解實(shí)現(xiàn)異步操作的方法步驟,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04java實(shí)現(xiàn)同態(tài)加密算法的實(shí)例代碼
這篇文章主要給大家介紹了關(guān)于java實(shí)現(xiàn)同態(tài)加密算法的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12談?wù)凧ava中整數(shù)類型(short int long)的存儲(chǔ)方式
在java中的整數(shù)類型有四種,分別是byte short in long,本文重點(diǎn)給大家介紹java中的整數(shù)類型(short int long),由于byte只是一個(gè)字節(jié)0或1,在此就不多說了,對(duì)java中的整數(shù)類型感興趣的朋友一起學(xué)習(xí)吧2015-11-11手動(dòng)添加jar包進(jìn)Maven本地庫(kù)內(nèi)的方法
這篇文章主要介紹了手動(dòng)添加jar包進(jìn)Maven本地庫(kù)內(nèi)的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08