1.前言
觀看這篇隨筆需要有spring security基礎。
心得: 1.生成token 的變化數據是用戶名和權限拼接的字符串 ,其他的固定 2.生成的token是將登錄通過的用戶的權限拼接的字符串加密后放入里面后加密,當攜帶token訪問時被攔截后,會將token解析出的權限注冊,因為不與數據庫等數據共享校驗權限最新信息, 如果在攜帶token的請求前權限有變化,但是token卻沒有改變,會導致token權限與用戶真實權限不一致,形成臟數據啦!!! 如果權限增加還好,使得無法訪問新加權限的操作,如果是減少權限,比如vip過期,用戶仍然可以有vip權限。 3.解決token臟數據的方案有兩個: (1)等待該token失效時間【不靠譜】; (2)每次修改權限時,會強制使得token失效,具體怎么做,還沒試過 4.當然,也有優點的,不與數據庫等最新數據做權限對比操作,較少了訪問數據庫該用戶信息的部分,能快速的過濾請求權限,理論上訪問數據會變快。 5.可以設置過期時間,單位毫秒,用時間戳設置 ,到時間則不可在使用, 但是缺點很明顯,在未過期之前,可以無數次訪問驗證通過,無法控制使用次數, 因此不能作為資源服務器對第三方應用開放的授權令牌, 6.令牌格式對不上,會直接報錯異常,為了服務降級,做個異常捕獲即可 7.如果生成了新的令牌,舊的令牌仍然可以使用,因此會導致多設備同時登錄的情況,無法控制登錄數量 8.使用jwt[java web token],做登錄校驗,則會導致http.sessionManagement().maximumSessions(1);設置失效,因為沒有使用session做為登錄控制 // 安全弊端很多 , 但是讓我深刻明白了token的內部思想
完全可以使用redis來完成用戶token的管理,但是這樣每次都需要向redis查詢一次,就讓jave web token 不倫不類了 。。。。
雖然已經盡可能的讓服務器減少負擔和提高反應速度,但仍然感覺好雞肋
//
當然應用場景還是有的,可用於分享連接的使用,這樣既能向有令牌的用戶使用被分享的權限,而且不需要去數據庫獲取數據對其進行對比,降低服務器負擔,
也就是說比較適合半開放性的功能使用
//
工程我放在GitHub倉庫了
https://github.com/cen-xi/spring-security-JWT
2.操作
看我的源碼大招,寫了很多注釋了,足夠詳細了,我懶得再寫說明
(1)目錄結構
(2)添加依賴包
pom.xml源碼

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.0.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>security-jwt-5605</artifactId> <version>0.0.1-SNAPSHOT</version> <name>security-jwt-5605</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <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>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <!-- jwt依賴--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
(3)做一個實體類,繼承 UserDetails

package com.example.securityjwt5605.model; 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.List; public class JwtUser implements UserDetails { //屬性名 username 和 password 是固定死的,不可更改,否則報錯 //但 grantedAuthorities 可隨意 private String username; private String password; private List<GrantedAuthority> grantedAuthorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return grantedAuthorities; } @Override public String getPassword() { return password; } @Override 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; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setGrantedAuthorities(List<GrantedAuthority> grantedAuthorities) { this.grantedAuthorities = grantedAuthorities; } }
(4)用戶名密碼登錄過濾器

package com.example.securityjwt5605.filters; import com.example.securityjwt5605.model.JwtUser; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.http.MediaType; 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.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * 首次登錄才調用這個方法 */ public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter { //構造方法 ,記得使用 public //第一個是 登錄路徑 。第二個是 認證管理者 //在啟動的時候就已經h已經執行了 public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) { super(new AntPathRequestMatcher(defaultFilterProcessesUrl)); System.out.println("===============================登錄攔截1=================================="); //存儲到父類,可不加 super.便於方法 attemptAuthentication()調用, setAuthenticationManager(authenticationManager); } /** *訪問/login登錄后首先進入這里 */ @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException, IOException, ServletException { JwtUser user = new JwtUser(); System.out.println("===============================登錄攔截2=================================="); try { //從請求中獲取用戶驗證信息 //將json字符串解析 user = new ObjectMapper().readValue(req.getInputStream(), JwtUser.class); // }catch (Exception ignored){ // //Exception ignored表示忽略異常 // //這樣內部可以不寫內容 // } // String username = req.getParameter("username"); // String password = req.getParameter("password"); // if (username == null || password == null){ // throw new Exception(); // } // user.setUsername(username); // user.setPassword(password); }catch (Exception e){ //Exception ignored表示忽略異常 System.out.println("請求無法解析出JwtUser對象"); } //對請求做認證操作,如何校驗,由默認的程序完成,不涉及對比操作,因為用戶信息存在內存中,否則需要修改 securityConfig.java 的 configure(AuthenticationManagerBuilder auth) 用於設置數據庫操作 //認證管理對象執行認證方法,new 一個用戶密碼認證令牌對象,參數為用戶名和密碼,然后放入認證方法中 //然后執行登錄驗證 return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())); } //認證成功 @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { System.out.println("===============================登錄攔截3=================================="); //獲取登錄角色的權限 //這是權限 ,如果登錄內存只有角色配置,無權限配置,則自動添加前綴構成權限 ROLE_角色 Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities(); //線程安全 StringBuffer stringBuffer = new StringBuffer(); for (GrantedAuthority grantedAuthority : authorities) { System.out.println("當前有的權限:"+grantedAuthority); //用逗號隔開好一點,不然后面需要手動切割 stringBuffer.append(grantedAuthority.getAuthority()).append(","); } //生成令牌 token String jwt = Jwts.builder() //登錄角色的權限,這會導致如果權限更改,該token無法及時更新權限信息 .claim("authorities", stringBuffer) //用戶名 .setSubject(authResult.getName()) //存活時間,過期則判為無效 .setExpiration(new Date(System.currentTimeMillis() + 1000 * 10)) //簽名,第一個參數時算法,第二個參數時內容,內容可隨意寫 .signWith(SignatureAlgorithm.HS512, "java521@java") //協議完成 .compact(); System.out.println(jwt); System.out.println("======================"); System.out.println(stringBuffer); //設置json數據返回給前端 Map<String, Object> map = new HashMap<>(); map.put("token", jwt); map.put("msg", "登錄成功"); //MediaType.APPLICATION_JSON_UTF8_VALUE 等用於 "application/json;charset=UTF-8" // response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.setContentType("application/json;charset=utf-8"); PrintWriter printWriter = response.getWriter(); //轉成json后傳送 printWriter.write(new ObjectMapper().writeValueAsString(map)); //關閉流 printWriter.flush(); printWriter.close(); } //認證失敗 @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { System.out.println("===============================登錄攔截4=================================="); Map<String, Object> map = new HashMap<>(); map.put("msg", "登錄失敗"); response.setContentType("application/json;charset=utf-8"); PrintWriter printWriter = response.getWriter(); //轉成json后傳送 printWriter.write(new ObjectMapper().writeValueAsString(map)); //關閉流 printWriter.flush(); printWriter.close(); } }
(5)token訪問過濾器

package com.example.securityjwt5605.filters; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.web.filter.GenericFilterBean; import sun.plugin.liveconnect.SecurityContextHelper; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.security.Security; import java.util.List; /** * 對攜帶token的請求做token檢查,對比是否正確,正確則可以直接通過 */ public class JwtFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("===============================token登錄攔截1=================================="); //強轉http請求 HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; //從請求頭獲取數據 //定死了名稱為 authorization String tokenStr = httpServletRequest.getHeader("authorization"); System.out.println(tokenStr); /* 打印結果 【不可換行,這里為了展示才換行】 Bearer eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfYWRtaW4sIiwic3ViIjoiYWRtaW4iLCJleHAi OjE1OTE2MzAwMTF9.oHmTl-f5RetmFJ8rM5MaIruOkA83sqt-6F7f2c27QRWdJvTAOIYX_VbRCngodaROZ4jprQ1ktwz5sZAWcDJdkg */ System.out.println("=========================================="); if (tokenStr != null) { System.out.println("有認證令牌"); boolean k = true; Jws<Claims> jws = null; try { //解析,解析方式使用加密時配置的數字簽名對應 //一旦令牌修改成位數對比不上,會報錯。。。 jws = Jwts.parser().setSigningKey("java521@java") .parseClaimsJws(tokenStr.replace("Bearer", "")); System.out.println(tokenStr.replace("Bearer", "")); /* 打印結果 【不可換行,這里為了展示才換行】 eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfYWRtaW4sIiwic3ViIjoiYWRtaW4iLCJleHAiOjE 1OTE2MzAwMTF9.oHmTl-f5RetmFJ8rM5MaIruOkA83sqt-6F7f2c27QRWdJvTAOIYX_VbRCngodaROZ4jprQ1ktwz5sZAWcDJdkg */ } catch (Exception e) { //放令牌被修改、時間過期,都會拋出異常,由方法 parseClaimsJws()安拋出的異常 // e.printStackTrace(); k = false; } if (k) { // 令牌解析成功 Claims claims = jws.getBody(); //獲取token解析出來的用戶名 String username = claims.getSubject(); System.out.println(username); /* 打印結果 [ROLE_admin,ROLE_user,等等] */ //從token獲取登錄角色的權限 //如果時以逗號格式配置字符串,可用以下方式解析,否則手動解析 List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities")); System.out.println(grantedAuthorities); // //new令牌登錄校驗 對象,參數分別是 : 用戶名 ,鹽[沒有則設為null] ,角色/權限 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, grantedAuthorities); //執行令牌登錄校驗 SecurityContextHolder.getContext().setAuthentication(token); } else { System.out.println("令牌解析失敗,被修改了"); SecurityContextHolder.getContext() .setAuthentication(new UsernamePasswordAuthenticationToken(null, null, null)); } } else { System.out.println("沒有認證令牌"); SecurityContextHolder.getContext() .setAuthentication(new UsernamePasswordAuthenticationToken(null, null, null)); } System.out.println("//讓過濾器繼續往下走,"); //讓過濾器繼續往下走 filterChain.doFilter(servletRequest, servletResponse); } } /* 總結: 1.生成token 的變化數據是用戶名和權限拼接的字符串 ,其他的固定 2.生成的token是將登錄通過的用戶的權限拼接的字符串加密后放入里面后加密,當攜帶token訪問時被攔截后,會將token解析出的權限注冊,因為不與數據庫等數據共享校驗權限最新信息, 如果在攜帶token的請求前權限有變化,但是token卻沒有改變,會導致token權限與用戶真實權限不一致,形成臟數據啦!!! 如果權限增加還好,使得無法訪問新加權限的操作,如果是減少權限,比如vip過期,用戶仍然可以有vip權限。 3.解決token臟數據的方案有兩個: (1)等待該token失效時間【不靠譜】; (2)每次修改權限時,會強制使得token失效,具體怎么做,還沒試過 4.當然,也有優點的,不與數據庫等最新數據做權限對比操作,較少了訪問數據庫該用戶信息的部分,能快速的過濾請求權限,理論上訪問數據會變快。 5.可以設置過期時間,單位毫秒,用時間戳設置 ,到時間則不可在使用, 但是缺點很明顯,在未過期之前,可以無數次訪問驗證通過,無法控制使用次數, 因此不能作為資源服務器對第三方應用開放的授權令牌, 6.令牌格式對不上,會直接報錯異常,為了服務降級,做個異常捕獲即可 7.如果生成了新的令牌,舊的令牌仍然可以使用,因此會導致多設備同時登錄的情況,無法控制登錄數量 8.使用jwt[java web token],做登錄校驗,則會導致http.sessionManagement().maximumSessions(1);設置失效,因為沒有使用session做為登錄控制 // 安全弊端很多 , 但是讓我深刻明白了token的內部思想 */
(6)服務層根據用戶名獲取用戶信息【為了簡便,沒有使用數據庫,直接賦值】

package com.example.securityjwt5605.filters; import com.example.securityjwt5605.model.JwtUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; 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; /** * 這個類其實就是為了獲取用戶的正確認證信息,不做信息比較,比較是在過濾器里面, * 名字叫做 UsernamePasswordAuthenticationFilter */ @Service public class MyUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { JwtUser tUser = new JwtUser(); //權限設置 List<GrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(); System.out.println("===============================數據庫層對比=================================="); if (username.equals("cen")) { tUser.setUsername(username); tUser.setPassword("$2a$10$Qghi7vHdyQJHYlAO.FCo/u3gCbqwWBVaSHjIF0Vci.C5.1l71SExq"); simpleGrantedAuthorities.add(new SimpleGrantedAuthority("ROLE_user")); tUser.setGrantedAuthorities(simpleGrantedAuthorities); } else if (username.equals("admin")) { tUser.setUsername(username); tUser.setPassword("$2a$10$ywq3gn6E15tnY3URptsIz.zn/fznWGqc2VhO4zphS/sIbWZJtLCVK"); simpleGrantedAuthorities.add(new SimpleGrantedAuthority("ROLE_admin")); tUser.setGrantedAuthorities(simpleGrantedAuthorities); } else { throw new UsernameNotFoundException("沒有找到用戶"); } // System.out.println("============================="); // //根據用戶名去數據庫查詢用戶信息 // TUser tUser = userService.getByUsername(username); // if (tUser == null){ // throw new UsernameNotFoundException("用戶不存在!"); // } // //權限設置 // List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(); // String role = tUser.getRole(); // //分割權限名稱,如 user,admin // String[] roles = role.split(","); // System.out.println("============================="); // System.out.println("注冊該賬戶權限"); // for (String r :roles){ // System.out.println(r); // //添加權限 // simpleGrantedAuthorities.add(new SimpleGrantedAuthority("ROLE_"+r)); //// simpleGrantedAuthorities.add(new SimpleGrantedAuthority(r)); // } // tUser.setGrantedAuthorities(simpleGrantedAuthorities); // System.out.println("============================="); /** * 創建一個用於認證的用戶對象,包括:用戶名,密碼,權限 * */ //輸入參數 // return new org.springframework.security.core.userdetails.User(tUser.getUsername(), tUser.getPassword(), simpleGrantedAuthorities); // 這個返回值的類型,繼承了userdetails即可 return tUser; } }
(7)contoller接口

package com.example.securityjwt5605.controller; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; @RestController public class HHController { //開啟跨域 // [普通跨域] //@CrossOrigin //[spring security 跨域] @CrossOrigin(allowCredentials = "true", allowedHeaders = "*") @RequestMapping("/hello") public Map<String, Object> hello() { Map<String, Object> map = new HashMap<>(); map.put("data", "hello"); return map; } //開啟跨域 // [普通跨域] //@CrossOrigin //[spring security 跨域] @CrossOrigin(allowCredentials = "true", allowedHeaders = "*") @RequestMapping("/admin") public Map<String, Object> admin() { Map<String, Object> map = new HashMap<>(); map.put("data", "i am admin"); return map; } }
(8)security配置類

package com.example.securityjwt5605.config; import com.example.securityjwt5605.filters.JwtFilter; import com.example.securityjwt5605.filters.JwtLoginFilter; import com.example.securityjwt5605.filters.MyUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; 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.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.lang.reflect.Method; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyUserDetailsService myUserDetailsService; /** * 全局的跨域配置 */ @Bean public WebMvcConfigurer WebMvcConfigurer() { return new WebMvcConfigurer() { public void addCorsMappings(CorsRegistry corsRegistry) { //僅僅讓/login可以跨域 corsRegistry.addMapping("/login").allowCredentials(true).allowedHeaders("*"); //僅僅讓/logout可以跨域 corsRegistry.addMapping("/logout").allowCredentials(true).allowedHeaders("*"); //允許所有接口可以跨域訪問 //corsRegistry.addMapping("/**").allowCredentials(true).allowedHeaders("*"); } }; } /** * 忽略過濾的靜態文件路徑 */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring() .antMatchers( "/js/**/*.js", "/css/**/*.css", "/img/**", "/html/**/*.html" ); } //內存放入可登錄的用戶信息 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { System.out.println("===============================認證管理構造器=================================="); //直接注冊信息到內存,會導致jrebel熱更新失效,無法更新該內容 // //如果僅僅設置了roles,則權限自動設置並自動添加前綴 為 ROLE_【角色內部的字符串,可以設置多個】, //字符串不可再添加ROLE_,會報java.lang.IllegalArgumentException: ROLE_user cannot start with ROLE_ (it is automatically added) //意思是用 ROLE_前綴會自動添加, // auth.inMemoryAuthentication().withUser("cen") // .password("$2a$10$Qghi7vHdyQJHYlAO.FCo/u3gCbqwWBVaSHjIF0Vci.C5.1l71SExq").roles("user") // //如果使用了roles 和 authorities ,那么roles將失效,將會注冊authorities內部的字符串為權限,且不會添加前綴名ROLE_ // .and().withUser("admin") // .password("$2a$10$ywq3gn6E15tnY3URptsIz.zn/fznWGqc2VhO4zphS/sIbWZJtLCVK").roles("user").authorities("ROLE_admin"); // // //因此用戶cen的權限為ROLE_user //用戶admin的權限為 admin // // //調用數據庫層,根據用戶名獲取用戶信息回來, auth.userDetailsService(myUserDetailsService) //設置加密方式 .passwordEncoder(passwordEncoder()); } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } //過濾規則,一旦設置了重寫了這個方法,必須設置登錄配置 //在啟動的時候就執行了 @Override protected void configure(HttpSecurity http) throws Exception { System.out.println("===============================過濾規則=================================="); http.authorizeRequests() .antMatchers("/hello").hasRole("user") .antMatchers("/admin").hasRole("admin") // .antMatchers("/admin").hasAuthority("admin") //當訪問/login的請求方式是post才允許通過 .antMatchers(HttpMethod.POST, "/login").permitAll() // .anyRequest() .anyRequest().authenticated() .and() //首次登錄攔截。僅允許post訪問/login .addFilterBefore(new JwtLoginFilter("/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class) //token驗證攔截 .addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class) // .cors() .and() .csrf().disable(); // //使用jwt[java web token],做登錄校驗,則該設置失效,因為沒有使用session做為登錄控制 // http.sessionManagement().maximumSessions(1); } }
(9)同時做了簡易的前端訪問頁面【前后端分離,前端端口是5601 ,后端是5605】
jwt.html

<!DOCTYPE html> <html lang="zh" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta charset="UTF-8"> <title>[JWT] 測試</title> </head> <body> jave web token [JWT] 測試 <hr> <div> <label> 賬戶: <input id="name" type="text"> </label> <label> 密碼: <input id="psw" type="text"> </label> </div> <button onclick="dosome(1)">登錄</button> <hr> <hr> <button onclick="dosome(3)">登出</button> <hr> <label> token: <input id="url" type="text" value="http://localhost:5605/hello"> <span id="token"></span> </label> <button onclick="dotoken()">點我token訪問</button> <hr> 返回的結果:<span id="res"></span> <!--當前路徑是/html/** ,因此需要返回一級 ,所以用 ../js/ --> <script src="../js/jquery-1.11.1.min.js"></script> <script src="../js/base64.js"></script> <script> //當前最新的token let token = ""; function dotoken() { let url = ""+($("#url").val()).trim(); if (url == ""){ console.log("url不可空") return; } $.ajax({ //請求頭添加token //方法一: // beforeSend: function (request) { // request.setRequestHeader("Authorization", token); // }, //方法二: headers: { //認證信息 Authorization: token }, async: true, type: 'post', dataType: "json", url: url, xhrFields: {withCredentials: true}, //前端適配:允許session跨域 crossDomain: true, success: function (data) { console.log(data); //請求成功回調函數 if (data != null) { // alert("有數據返回") $("#res").html(JSON.stringify(data)) } else { alert("系統異常") } }, error: function (xhr, type, errorThrown) { //異常處理; console.log("異常處理") console.log(JSON.stringify(xhr)); if (xhr.readyState == 4 && xhr.status == 403){ $("#res").html("403無權訪問") } /* {"readyState":4,"responseText":"{\"timestamp\":\"2020-06-08T16:51:15.016+00:00\", \"status\":403,\"error\":\"Forbidden\",\"message\":\"\",\"path\":\"/admin\"}", "responseJSON":{"timestamp":"2020-06-08T16:51:15.016+00:00","status":403,"error":"Forbidden", "message":"","path":"/admin"},"status":403,"statusText":"error"} */ console.log(type); console.log(errorThrown); } }); } function dosome(type) { let name = ""; let psw = ""; let url = ""; if (type == 1) { name = ($("#name").val()).trim(); psw = ($("#psw").val()).trim(); //登錄 url = "http://localhost:5605/login"; } else if (type == 3) { //登出 url = "http://localhost:5605/logout"; } //URL是URI的子集,所有的URL都是URI,但不是每個URI都是URL,還有可能是URN。 $.ajax({ async: true, type: 'post', //對應於后端 parama 方式獲取數據 ,使用req.getParameter獲取 // data: {"username": name, "password": psw}, //對應於后端raw方式獲取數據,需要json解析,使用req.getInputStream()獲取 data: JSON.stringify({"username": name, "password": psw}), //這里類型是json,那么跨域的后端需要是map類型、po實體類等 json類型 才能接收數據 dataType: "json", url: url, xhrFields: {withCredentials: true}, //前端適配:允許session跨域 crossDomain: true, // //請求頭設置 // headers: { // //認證信息 // Authorization: authorization // }, success: function (data) { console.log(data); //請求成功回調函數 if (data != null) { // alert("有數據返回") $("#res").html(JSON.stringify(data)) token = data.token; $("#token").html(token); } else { alert("系統異常") } }, error: function (xhr, type, errorThrown) { //異常處理; console.log("異常處理") console.log(JSON.stringify(xhr)); console.log(type); console.log(errorThrown); } }); } </script> </body> </html>
3.測試
token時間設長一點,我這里設為1小時
(1)使用postman f訪問 http://localhost:5605/login 進行登錄
登錄成功
獲取令牌,訪問 http://localhost:5605/hello
提交后
成功
因為用戶 cen我設置了只有權限。
再次訪問 http://localhost:5605/admin
無權限403被拒絕了
事實上,當令牌過期后再訪問,也會拋出403結果
換一個賬戶
訪問 http://localhost:5605/admin ,是可以訪問的
(2)測試前端 跨域 訪問
測試密碼錯誤
測試登錄成功
點擊token訪問
token 成功
換一個沒有訪問hello權限的賬號
然后再次點擊token訪問
4.過濾器的先后操作
(1)工程啟動,控制台打印
(2)用戶名密碼登錄后,控制台打印
(3)攜帶token訪問
有效的令牌
無效的令牌
奇怪的是 攜帶無效令牌時 會執行兩次token訪問過濾器,原因還不清楚
-------------------------
參考博文原址:
https://mp.weixin.qq.com/s/Sn59dxwtsEWoj2wdynQuRQ