spring boot + spring security +JWT令牌 +前后端分離--- 心得


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>
View Code

 

(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;
    }
}
View Code

 

(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();
    }
}
View Code

 

 (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的內部思想
 */
View Code

 

(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;

    }


}
View Code

 

(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;
    }


}
View Code

 

(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);


    }


}
View Code

 

(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>
View Code

 

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM