首页 > 技术文章 > spring boot + spring security +JWT令牌 +前后端分离--- 心得

c2g5201314 2020-06-09 02:11 原文

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

 

推荐阅读