安全框架SpringSecurity-2(集成thymeleaf&集成验证码&JWT)

一、SpringSecurity 集成thymeleaf

①:复制并修改工程

  • 复制04_spring_security并重命名为05_spring_security_thymeleaf

②:添加配置和依赖

  • 添加thymeleaf依赖
xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  • 修改application.yml
yaml 复制代码
spring:
  thymeleaf:
    cache: false # 不使用缓存
	check-template: true  # 检查thymeleaf模板是否存在

③:添加thymeleaf模板和插件

  1. 添加thymeleaf模板
  • 模板名称thymeleaf ,扩展名html,具体内容如下:
html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>#[[$Title$]]#</title>        
</head>
<body>
#[[$END$]]#
</body>
</html>
  • 简要说明:
    #[[$Title$]]# #[[$END$]]# 这两处的作用是,当你新建一个模板页面时,在<title>标签中输入标题内容后,只需要点击回车键,光标就会直接跳到<body>内,省去了你挪动鼠标,或者挪动方向键的步骤,也可以给你节省一点点时间。

2.idea安装html转thymeleaf的插件JBLHtmlToThymeleaf

  • 安装JBLHtmlToThymeleaf插件

④:新建Controller层代码

1.新建LoginController

java 复制代码
@Controller
@RequestMapping("/login")
public class LoginController {
    
    /*
     * 跳转到登录页面
     * @DateTime: 2023/11/11 17:57
     *
     * @return String
     * @author: Coke
     */
    @RequestMapping("/toLogin")
    public String toLogin(){
        return "login";
    }
}

2.新建IndexController

java 复制代码
@Controller
@RequestMapping("/index")
public class IndexController {
    
    
    /*
     * 登录成功后进入页面
     * @DateTime: 2023/11/11 17:58
     *
     * @return String
     * @author: Coke
     */
    @RequestMapping("/toIndex")
    public String toIndex(){
        return "main";
    }
}

⑤:创建静态页面

1.创建login.html

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户登陆</title>
</head>
<body>
<h2>登录页面</h2>
<form action="/login/toLogin" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="uname" value="thomas"></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="pwd"></td>
            <span th:if="${param.error}">用户名或者密码错误</span>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登录</button>
            </td>
        </tr>
    </table>
</form>
</body>

2.创建main.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>系统首页</title>
</head>
<body>
<h1 align="center">系统首页</h1>
<a href="/student/query">查询学生</a>
<br>
<a href="/student/add">添加学生</a>
<br>
<a href="/student/update">更新学生</a>
<br>
<a href="/student/delete">删除学生</a>
<br>
<a href="/student/export">导出学生</a>
<br>
<br><br><br>
<h2><a href="/logout">退出</a></h2>
<br>
</body>
</html>

⑥:修改安全配置文件WebSecurityConfig

java 复制代码
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 对密码进行编码
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Override
    protected void configure (HttpSecurity http) throws Exception {
        // 任何请求 都需要登录,注意:没有配置mV℃匹配器的只要登录成功就可以访问
        http.authorizeRequests().anyRequest().authenticated();
        http.formLogin()
              .loginPage("/login/toLogin") // 配置自定义的登录页面
              .usernameParameter("uname") // 指定登录页面的用户名字段
              .passwordParameter("pwd") // 指定登录页面的密码字段
              .loginProcessingUrl("/login/toLogin") // 配置点击登录时的请求的url
              .successForwardUrl("/index/toIndex") // 登录成功后跳转的页面
              .failureForwardUrl("/login/toLogin") // 登录失败后跳转的页面
              .permitAll();
        
        // 配置登出方式
        http.logout()
              .logoutUrl("/logout")
              .logoutSuccessUrl("/login/toLogin")
              .permitAll();
        //  禁用csrf跨域请求攻击
        http.csrf().disable();
    }
}

⑦:修改Studentcontroller

java 复制代码
@Controller
@Slf4j
@RequestMapping("/student")
public class StudentController {
    @GetMapping("/query")
    @PreAuthorize("hasAnyAuthority('student:query')")
    public String queryInfo(){
        return "user/query";
    }
    
    @GetMapping("/add")
    @PreAuthorize("hasAnyAuthority('student:add')")
    public String addInfo(){
        return "user/add";
    }
    
    @GetMapping("/update")
    @PreAuthorize("hasAnyAuthority('student:update')")
    public String updateInfo(){
        return "user/update";
    }

    @GetMapping("/delete")
    @PreAuthorize("hasAnyAuthority('student:delete')")
    public String deleteInfo(){
        return "user/delete";
    }
    
    @GetMapping("/export")
    @PreAuthorize("hasAnyAuthority('student:export')")
    public String exportInfo(){
        return "user/export";
    }
}

⑧:在templates/user下面创建学生管理的各个页面

1.创建export.html 导出页面

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
	<head>
	    <meta charset="UTF-8">
	    <title>系统首页-学生管理</title>
	</head>
	<body>
	    <h1 align="center">系统首页-学生管理-导出</h1>
	        <a href="/index/toIndex">返回</a>
	    <br>
	</body>
</html>

2.创建add.html 添加页面

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
	<head>
	    <meta charset="UTF-8">
	    <title>系统首页-学生管理</title>
	</head>
	<body>
	    <h1 align="center">系统首页-学生管理-新增</h1>
	        <a href="/index/toIndex">返回</a>
	    <br>
	</body>

</html>

3创建delete.html 删除页面

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
	<head>
	    <meta charset="UTF-8">
	    <title>系统首页-学生管理</title>
	</head>
    <body>
        <h1 align="center">系统首页-学生管理-删除</h1>
            <a href="/index/toIndex">返回</a>
        <br>
    </body>
</html>

4.创建update.htm 修改l页面

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>系统首页-学生管理</title>
    </head>
    <body>
        <h1 align="center">系统首页-学生管理-更新</h1>
            <a href="/index/toIndex">返回</a>
        <br>
    </body>
</html>

5.创建query.html 查询页面

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>系统首页-学生管理</title>
    </head>
    <body>
        <h1 align="center">系统首页-学生管理-查询</h1>
            <a href="/index/toIndex">返回</a>
        <br>
    </body>
</html>

⑨:创建403页面 启动测试

1.在static/error下面创建403.html

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>403</title>
    </head>
    <body>
        <h2>403:你没有权限访问此页面</h2>
        <a href="/index/toIndex">去首页</a>
    </body>
</html>

2.启动测试

  • 01.使用thomas 用户登录 并查看用户该用户的权限
  • 测试学生查询权限
  • 测试学生的导出权限

⑩:当用户没有某权限时,页面不展示该按钮

我们之前创建的项目里面是当用户点击页面上的链接请求到后台之后没有权限会跳转到403,那么如果用户没有权限,对应的按钮就不显示出来,这样岂不是更好吗我们接着上一个项目来改造

1.引入下面的依赖

xml 复制代码
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>

2.修改main.html即可

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
    <head>
        <meta charset="UTF-8">
        <title>系统首页</title>
    </head>
    <body>
        <h1 align="center">系统首页</h1>
        <a href="/student/query" sec:authorize="hasAuthority('student:query')" >查询用户</a>
        <br>
            <a href="/student/add" sec:authorize="hasAuthority('student:add')" >添加用户</a>
        <br>
            <a href="/student/update" sec:authorize="hasAuthority('student:update')" >更新用户</a>
        <br>
            <a href="/student/delete" sec:authorize="hasAuthority('student:delete')" >删除用户</a>
        <br>
            <a href="/student/export" sec:authorize="hasAuthority('student:export')" >导出用户</a>
        <br>
        <br><br><br>
            <h2><a href="/logout">退出</a></h2>
        <br>
    </body>
</html>

3.重启启动登录后查看效果

二、springsecurity 集成图片验证码

  • 复制上一个工程05_spring_security_thymeleaf,修改名字06_spring_security_captcha

①:原理、存在问题、解决思路

我们知道Spring Security是通过过滤器链来完成了,所以它的解决方案是创建一个过滤器放到Security的过滤器链中,在自定义的过滤器中比较验证码

②:添加依赖(用于生成验证码)

xml 复制代码
        <!--引入hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.9</version>
        </dependency>

③:添加一个获取验证码的接口

java 复制代码
@Controller
@Slf4j
public class CaptchaController {
    
    @GetMapping("/code/image")
    public void getCaptcha(HttpServletRequest request, HttpServletResponse response){
        // 创建一个验证码  width -- 图片宽 height -- 图片高 codeCount -- 字符个数 circleCount -- 干扰圆圈条数
        CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 20);
        // 放到session中
        String code = circleCaptcha.getCode();
        request.getSession().setAttribute("CAPTCHA_CODE",code);
        log.info("图片验证码为:{}", code);
    
        try {
            ImageIO.write(circleCaptcha.getImage(),"JPEG",response.getOutputStream());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

④:创建验证码过滤器

java 复制代码
@Component
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        log.info("请求路径为:{}",requestURI);
        
        // 判断请求的是否为登录页面
        if (!"/login/doLogin".equals(requestURI)){ // 不是登录请求,直接放行
            doFilter(request,response,filterChain); //直接下一个
            return;
        }
        //校验验证码
        validateCode(request,response,filterChain);
    }
    
    private void validateCode (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
       // 获取用户输入的验证码
        String code = request.getParameter("code");
        // 获取session中存储的验证码
        HttpSession session = request.getSession();
        String login_captcha_code = (String) session.getAttribute("CAPTCHA_CODE");
        log.info("用户输入的验证码: {}, session中存储的验证码: {}", code, login_captcha_code);
        
        // 移除错误信息
        session.removeAttribute("captchaCodeErrorMsg");
        if (!StringUtils.hasText(code)){
            request.getSession().setAttribute("captchaCodeErrorMsg", "请输入验证码!");
            // 重定向到登录页面
            response.sendRedirect("/login/toLogin");
            return;
        }
        
        if (!StringUtils.hasText(login_captcha_code)){
            request.getSession().setAttribute("captchaCodeErrorMsg", "验证码错误!");
            // 重定向到登录页面
            response.sendRedirect("/login/toLogin");
        }
        
        if (!code.equalsIgnoreCase(login_captcha_code)){
            request.getSession().setAttribute("captchaCodeErrorMsg", "验证码输入错误!");
            // 重定向到登录页面
            response.sendRedirect("/login/toLogin");
        }
        // 删除session中的验证码
        session.removeAttribute("code");
        this.doFilter(request,response,filterChain);
    }
}

⑤: 修改WebSecurityConfig(重点)

java 复制代码
@Configuration
@EnableGlobalMethodSecurity (prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private ValidateCodeFilter validateCodeFilter;
    
    // 对密码进行编码
    @Bean
    public PasswordEncoder passwordEncoder () {
        return new BCryptPasswordEncoder();
    }
    
    
    @Override
    protected void configure (HttpSecurity http) throws Exception {
        // 配置登录之前添加一个验证码的过滤器
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
        // 配置路径拦截 的 url的匹配规则
        //所有请求,都需要认证
        http.authorizeRequests()
              .mvcMatchers("/code/image")
              .permitAll() //放开验证码的请求
              .anyRequest().authenticated();
        http.formLogin()
              .loginPage("/login/toLogin") // 配置自定义的登录页面
              .usernameParameter("uname") // 指定登录页面的用户名字段
              .passwordParameter("pwd") // 指定登录页面的密码字段
              .loginProcessingUrl("/login/toLogin") // 配置点击登录时的请求的url
              .successForwardUrl("/index/toIndex") // 登录成功后跳转的页面
              .failureForwardUrl("/login/toLogin") // 登录失败后跳转的页面
              .permitAll();
        
        // 配置登出方式
        http.logout()
              .logoutUrl("/logout")
              .logoutSuccessUrl("/login/toLogin")
              .permitAll();
        //  禁用csrf跨域请求攻击
        http.csrf().disable();
    }
    
    
    /**
     * 资源服务匹配放行【静态资源文件】
     *
     * @param web
     * @throws Exception
     */
    // @Override
    //public void configure(WebSecurity web) throws Exception {
    //  web.ignoring().mvcMatchers("/resources/**");
    //}
    
}

⑥:修改login.html 后 进行测试

1.修改login.html

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户登陆</title>
</head>
<body>
<h2>登录页面</h2>
<!--${param.error}这个如果有值,就显示帐号或密码错误-->
<h4 th:if="${param.error}" style="color: #FF0000;">帐号或密码错误,请重新输入</h4>
<form action="/login/doLogin" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="uname" value="thomas"></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="pwd"></td>
            <span th:if="${param.error}">用户名或者密码错误</span>
        </tr>
        <tr>
            <td>验证码:</td>
            <td><input type="text" name="code"> <img src="/code/image" style="height:33px;cursor:pointer;" onclick="this.src=this.src">
                <span th:text="${session.captchaCodeErrorMsg}" style="color: #FF0000;" >username</span>
            </td>
        </tr>

        <tr>
            <td colspan="2">
                <button type="submit">登录</button>
            </td>
        </tr>
    </table>
</form>
</body>

2.测试

1.故意输入错误

2.登录成功

三、base64编码和JWT

①:什么是Base64

所谓Base64,就是说选出64个字符:小写字母a-z、大写字母A-Z、数字0-9、符号"+"、"/"(再加上作为垫字的"=",实际上是使用65个字符),作为一个基本字符集。然后,其他所有符号都转换成这个字符集中的字符。

1.Base64和Base64Url 的区别

Base64Url是一种在Base64的基础上编码形成新的编码方式,为了编码能在网络中安全顺畅传输,需要对Base64进行的编码,特别是互联网中。

  • Base64Url 编码的流程:

    1、明文使用BASE64进行编码
    2、在Base64编码的基础上进行以下的处理:
    1)去除尾部的"="
    2)把"+"替换成"-"
    3)斜线"/"替换成下划线"_"

②:跨域认证问题和JWT 实现登录原理图

1.跨域认证问题

互联网服务离不开用户认证。一般流程是下面这样。

  • 用户向服务器发送用户名和密码。
  • 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
  • 服务器向用户返回一个 jsession_id,写入用户的 Cookie。
  • 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
  • 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。

举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
一种解决方案是 session 数据持久化 ,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。 服务器不存数据,客户端存,服务器解析就行了

2.JWT 实现登录原理图


说明:

JWT只通过算法实现对Token合法性的验证,不依赖数据库,Memcached的等存储系统,因此可以做到跨服务器验证,只要密钥和算法相同,不同服务器程序生成的Token可以互相验证。

③:JWT学习

1.简介

JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,用于在各方之间作为JSON对象安全地传输信息。 此信息可以通过数字签名进行验证和信任。 JWT可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。

2.用途

授权: 这是我们使用JWT最广泛的应用场景。一次用户登录,后续请求将会包含JWT,对于那些合法的token,允许用户连接路由,服务和资源。目前JWT广泛应用在SSO(Single Sign On)(单点登录)上。因为他们开销很小并且可以在不同领域轻松使用。

信息交换: JSON Web Token是一种在各方面之间安全信息传输的好的方式 因为JWT可以签名 - 例如,使用公钥/私钥对 - 您可以确定发件人是他们所说的人。 此外,由于使用标头和有效负载计算签名,您还可以验证内容是否未被篡改

3.JWT组成

一个JWT由三部分组成,各部分以点分隔:

复制代码
Header(头部)-----base64Url编码的Json字符串
Payload(载荷)---base64url编码的Json字符串
Signature(签名)---使用指定算法,通过Header和Playload加盐计算的字符串

一个JWT看起来像下面这样:

复制代码
xxxxx.yyyyy.zzzzz

下面这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
01.Header

此部分有两部分组成:

  • 一部分是token的类型,目前只能是JWT
  • 另一部分是签名算法,比如HMAC 、 SHA256 、 RSA

示例:

复制代码
{
  "alg":"HS256",
  "typ":"JWT"
}

base64编码命令:

复制代码
echo -n '{"alg":"HS256","typ":"JWT"}' | base64
02.Payload

token的第二部分是payload(有效负载),其中包含claims(声明)。Claims是关于一个实体(通常是用户)和其他数据类型的声明。

claims有三种类型:registered,public,and private claims。

Registered(已注册的声明):这些是一组预定义声明,不是强制性的,但建议使用,以提供一组有用的,可互操作的声明。 其中一些是:iss(发行人),exp(到期时间),sub(主题),aud(观众)and others。(请注意,声明名称只有三个字符,因为JWT意味着紧凑。)

复制代码
JWT 规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息(密码,手机号等)放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。

Public(公开声明):这些可以由使用JWT的人随意定义。 但为避免冲突,应在IANA JSON Web Token Registry中定义它们,或者将其定义为包含防冲突命名空间的URI。

private (私人声明):这些声明是为了在同意使用它们的各方之间共享信息而创建的,并且既不是注册声明也不是公开声明

复制代码
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
03.Signature(保证数据安全性的)

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(

base64UrlEncode(header) + "." +

base64UrlEncode(payload),

secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

示例:

复制代码
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

4. JWT 的使用方式【重点】

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

Authorization: Bearer jwt

另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面

5.JWT 的几个特点

JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

JWT 不加密的情况下,不能将秘密数据写入 JWT。

JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑(JWT的登出问题)。就是因为服务端无状态了

正常情况下 修改了密码后就会跳转到登录页面 :修改成功后清空浏览器保存的token了

后端怎么玩? 因为服务端不保留token 我用之前的token 还是可以继续访问的

从有状态(后端也会存一个)的变成无状态的了

我们就要把它从无状态再变成有状态了

JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

为了减少盗用,JWT 不应该使用 HTTP 80 协议明码传输,要使用 HTTPS 443 协议传输。

我们颁发一个令牌 用户名称 用户的权限信息 这个令牌2个小时有效

Jwt只要能解析 就认为你是可用的 做不了 登出 后端不存储用户信息了 后端无状态了

④:Java类库



⑤:java中使用jwt

1.新建maven工程jwt-learn1

2.引入依赖

xml 复制代码
<!-- 添加jwt的依赖 -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.11.0</version>
</dependency>

3.编写功能类

java 复制代码
package com.it.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Date;
import java.util.HashMap;
import java.util.List;

/**
 * @Author: CaoYouGen
 * @DateTime: 2023/11/13/12:03
 * @注释: 用于生成和解析JWT
 **/
public class UtilJWT {
    
    // 声明一个密钥
    private static final String SECRET = "Coke_Anne";

    /*
     * TODO
     * 
     * @param userId: 用户编号
     * @param userName: 用户名
     * @param auth: 用户权限  
     * @return String
     * @author: CaoYouGen
     * @DateTime: 2023/11/13 12:06
     */
    public static String createToken(Integer userId, String userName, List<String> auth){
        // 得到当前的系统时间
        Date currentDate = new Date();
        // 根据当前时间计算出过期时间 定死为5分钟
        Date expTime = new Date(currentDate.getTime() + (1000 * 60 * 5));
        // 组装头数据
        HashMap<String, Object> header = new HashMap<>();
        header.put("alg","HS256");
        header.put("typ","JWT");
        return JWT.create()
                .withHeader(header) // 头
                .withIssuedAt(currentDate) // 创建时间
                .withExpiresAt(expTime) // 过期时间
                .withClaim("userId",userId) // 自定义数据
                .withClaim("userName",userName) // 自定义数据
                .withClaim("auth",auth) // 自定义数据
                .sign(Algorithm.HMAC256(SECRET));
    }
    
    public static Boolean verifyToken(String token){
        try {
            // 使用密码创建一个解析对象
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
            // 验证 JWT
            DecodedJWT decodedJWT = jwtVerifier.verify(token);
            return true;
        }catch (TokenExpiredException e){
            e.printStackTrace();
        }
        return false;
    }
    
    /*
     * 获取JWT里面相前的用户编号
     * 
     * @param token:  
     * @return Integer
     * @author: CaoYouGen
     * @DateTime: 2023/11/13 12:19
     */
    public static Integer getIUserId(String token){
        try {
            // 使用密码创建一个解析对象
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
            // 验证 JWT
            DecodedJWT decodedJWT = jwtVerifier.verify(token);
            Claim userId = decodedJWT.getClaim("userId");
            return userId.asInt();
        }catch (TokenExpiredException e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取JWT里面相前的用户名
     */
    public static String getUsername(String token){
        try{
            // 使用秘钥创建一个解析对象
            JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
            //验证JWT
            DecodedJWT decodedJWT = jwtVerifier.verify(token);
            Claim username = decodedJWT.getClaim("userName");
            return username.asString();
        }catch (TokenExpiredException e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取JWT里面相前权限
     */
    public static List<String> getAuth(String token){
        try{
            // 使用秘钥创建一个解析对象
            JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
            //验证JWT
            DecodedJWT decodedJWT = jwtVerifier.verify(token);
            Claim auth = decodedJWT.getClaim("auth");
            return auth.asList(String.class);
        }catch (TokenExpiredException e){
            e.printStackTrace();
        }
        return null;
    }



}

4.测试一下

java 复制代码
package com.it.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Date;
import java.util.HashMap;
import java.util.List;

/**
 * @Author: CaoYouGen
 * @DateTime: 2023/11/13/12:03
 * @注释: 用于生成和解析JWT
 **/
public class UtilJWT {
    
    // 声明一个密钥
    private static final String SECRET = "Coke_Anne";

    /*
     * TODO
     * 
     * @param userId: 用户编号
     * @param userName: 用户名
     * @param auth: 用户权限  
     * @return String
     * @author: CaoYouGen
     * @DateTime: 2023/11/13 12:06
     */
    public static String createToken(Integer userId, String userName, List<String> auth){
        // 得到当前的系统时间
        Date currentDate = new Date();
        // 根据当前时间计算出过期时间 定死为5分钟
        Date expTime = new Date(currentDate.getTime() + (1000 * 60 * 5));
        // 组装头数据
        HashMap<String, Object> header = new HashMap<>();
        header.put("alg","HS256");
        header.put("typ","JWT");
        return JWT.create()
                .withHeader(header) // 头
                .withIssuedAt(currentDate) // 创建时间
                .withExpiresAt(expTime) // 过期时间
                .withClaim("userId",userId) // 自定义数据
                .withClaim("userName",userName) // 自定义数据
                .withClaim("auth",auth) // 自定义数据
                .sign(Algorithm.HMAC256(SECRET));
    }
    
    public static Boolean verifyToken(String token){
        try {
            // 使用密码创建一个解析对象
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
            // 验证 JWT
            DecodedJWT decodedJWT = jwtVerifier.verify(token);
            return true;
        }catch (TokenExpiredException e){
            e.printStackTrace();
        }
        return false;
    }
    
    /*
     * 获取JWT里面相前的用户编号
     * 
     * @param token:  
     * @return Integer
     * @author: CaoYouGen
     * @DateTime: 2023/11/13 12:19
     */
    public static Integer getIUserId(String token){
        try {
            // 使用密码创建一个解析对象
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
            // 验证 JWT
            DecodedJWT decodedJWT = jwtVerifier.verify(token);
            Claim userId = decodedJWT.getClaim("userId");
            return userId.asInt();
        }catch (TokenExpiredException e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取JWT里面相前的用户名
     */
    public static String getUsername(String token){
        try{
            // 使用秘钥创建一个解析对象
            JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
            //验证JWT
            DecodedJWT decodedJWT = jwtVerifier.verify(token);
            Claim username = decodedJWT.getClaim("userName");
            return username.asString();
        }catch (TokenExpiredException e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取JWT里面相前权限
     */
    public static List<String> getAuth(String token){
        try{
            // 使用秘钥创建一个解析对象
            JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
            //验证JWT
            DecodedJWT decodedJWT = jwtVerifier.verify(token);
            Claim auth = decodedJWT.getClaim("auth");
            return auth.asList(String.class);
        }catch (TokenExpiredException e){
            e.printStackTrace();
        }
        return null;
    }
}

⑥:JWT的总结

JWT就是一个加密的带用户信息的字符串, 没学习JWT之前,我们在项目中都是返回一个基本的字符串,然后请求时带上这个字符串,再从session或者redis中(共享session)获取当前用户,学过JWT以后我们可以把用户信息直接放在字符串返回给前端,然后用户请求时带过来,我们是在服务器进行解析拿到当前用户,这就是两种登录方式,这两种方式有各自的优缺点。

四、JWT+Spring Security+redis+mysql 实现认证

①:新建模块添加依赖和配置

1.复制工程06_spring_security_captcha,改名字为

08_spring_security_jwt
2.添加jwt依赖

xml 复制代码
<!-- 添加jwt的依赖 -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.11.0</version>
</dependency>

3.application.yml 中配置密钥

yaml 复制代码
jwt:
  secretKey: Coke_Anne

②:jwt功能类

java 复制代码
package com.it.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.List;

/**
 * @Author: CaoYouGen
 * @DateTime: 2023/11/13/12:35
 * @注释: TODO
 **/

@Component
@Slf4j
public class UtilJWT {
    @Value ("${jwt.secretKey}")
    private String jwtSecretKey;
    
    /*
     * 创建jwt
     *
     * @param userInfo: 用户信息
     * @param authList:  用户权限列表
     * @return String 返回jwt(Json Web Token)
     * @author: CaoYouGen
     * @DateTime: 2023/11/13 12:39
     */
    public String createToken (String userInfo, List<String> authList) {
        // 创建时间
        Date currentTime = new Date();
        // 过期时间,5分钟后过期
        Date expiretime = new Date(currentTime.getTime() + (1000 * 60 * 5));
        
        // jwt的header信息
        HashMap<String, Object> header = new HashMap<>();
        header.put("alg", "HS256");
        header.put("typ", "JWT");
        return JWT.create()
              .withHeader(header) // 设置头部信息
              .withIssuedAt(currentTime) // 设置签发日期(创建时间)
              .withExpiresAt(expiretime) // 设置过期时间
              .withIssuer("thomas") // 设置签发人
              .withClaim("userInfo", userInfo) // 私有声明 可以自己定义
              .withClaim("authList", authList) // 私有声明 可以自己定义
              .sign(Algorithm.HMAC256(jwtSecretKey)); // 签名 使用HS256算法签名,并使用密钥
        // HS256是一种对称算法,这意味着只有一个密钥,在双方之间共享。 使用相同的密钥生成签名并对其进行验证。 应特别注意钥匙是否保密。
    }
    
    /*
     * 验签jwt的签名简称验签
     * @DateTime: 2023/11/13 19:09
     *
     * @param token: 需要验签的token
     * @return boolean 验签结果
     * @author: Coke
     */
    public boolean verifyToken (String token) {
        // 创建一个验签类对象
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build();
        try {
            // 验签,如果不报错,则说明 jwt 是合法的,而且也没有过期
            DecodedJWT verify = jwtVerifier.verify(token);
            return true;
        } catch (JWTVerificationException e) {
            // 如果报错说明 jwt 为非法的,或者已过期(已过期也属于非法的)
            e.printStackTrace();
            log.error("验证失败:{}", token);
        }
        return false;
    }
    
    /*
     * 获取用户信息
     * @DateTime: 2023/11/13 19:20
     *
     * @param token: jwt
     * @return String 用户信息
     * @author: Coke
     */
    public String getUserInfo (String token) {
        // 创建一个jwt验签对象
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build();
        try {
            // 验签
            DecodedJWT verify = jwtVerifier.verify(token);
            String userInfo = verify.getClaim("userInfo").asString();
            return userInfo;
        } catch (JWTCreationException e) {
            e.printStackTrace();
        }
        return null;
    }
    
    /*
     * 获取用户的权限列表
     * @DateTime: 2023/11/13 19:25
     *
     * @param token: jwt
     * @return List<String> 权限列表
     * @author: Coke
     */
    public List<String> getUserAuth (String token) {
        // 创建一个jwt验签对象
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build();
        try {
            DecodedJWT verify = jwtVerifier.verify(token);
            List<String> authList = verify.getClaim("authList").asList(String.class);
            return authList;
        }catch (JWTCreationException e){
            e.printStackTrace();
            return null;
        }
    }
}

③:添加响应类

java 复制代码
package com.it.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * TODO
 *
 * @author: Coke
 * @DateTime: 2023/11/13/19:27
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class HttpResult implements Serializable {
    private Integer code; // 响应码
    private String msg; // 响应消息
    private Object data; // 响应对象
}

④:修改SecurityUser类

加入一个获取SysUser的方法

java 复制代码
public SysUser getSysUser() {
    return sysUser;
}

⑤:新建认证成功处理器

java 复制代码
package com.it.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.it.entity.SysUser;
import com.it.utils.UtilJWT;
import com.it.vo.HttpResult;
import com.it.vo.SecurityUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

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.List;
import java.util.stream.Collectors;

/**
 * TODO
 *
 * @author: Coke
 * @DateTime: 2023/11/13/19:33
 **/
@Component
@Slf4j
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    // 使用此工具进行序列化
    @Autowired
    private ObjectMapper objectMapper;
    
    @Autowired
    private UtilJWT utilJWT;
    
    @Override
    public void onAuthenticationSuccess (HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        // 从认证对象中获取认证用户信息
        SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
        SysUser sysUser = securityUser.getSysUser();
        String userInfo = objectMapper.writeValueAsString(sysUser);
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) securityUser.getAuthorities();
        // 使用stream流
        List<String> authList = authorities.stream().map(SimpleGrantedAuthority :: getAuthority).collect(Collectors.toList());
        // 创建jwt
        String token = utilJWT.createToken(userInfo, authList);
        log.info("token= {}",token);
        // 返回给前端 token
        HttpResult httpResult = HttpResult.builder()
              .code(1)
              .msg("jwt验证成功!")
              .data(token).build();
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.write(objectMapper.writeValueAsString(httpResult));
        writer.flush();
    }
}

⑥:新建jwt过滤器,用于检查token等

java 复制代码
package com.it.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.it.entity.SysUser;
import com.it.utils.UtilJWT;
import com.it.vo.HttpResult;
import com.it.vo.SecurityUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.stream.Collectors;

/**
 * TODO
 *
 * @author: Coke
 * @DateTime: 2023/11/12/14:07
 **/
@Component
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter {
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Autowired
    private UtilJWT utilJWT;
    
    @Override
    protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        log.info("请求路径为:{}",requestURI);
        
        // 判断请求的是否为登录页面
        if (!"/login/doLogin".equals(requestURI)){ // 不是登录请求,直接放行
            doFilter(request,response,filterChain); //直接下一个
            return;
        }
        //校验验证码
        validateCode(request,response,filterChain);
    }
    
    private void validateCode (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
       // 获取用户输入的验证码
        String code = request.getParameter("code");
        // 获取session中存储的验证码
        HttpSession session = request.getSession();
        String login_captcha_code = (String) session.getAttribute("CAPTCHA_CODE");
        log.info("用户输入的验证码: {}, session中存储的验证码: {}", code, login_captcha_code);
        
        // 移除错误信息
        session.removeAttribute("captchaCodeErrorMsg");
        
        if (!StringUtils.hasText(code)){
            request.getSession().setAttribute("captchaCodeErrorMsg", "请输入验证码!");
            // 重定向到登录页面
            response.sendRedirect("/login/toLogin");
            return;
        }
        
        if (!StringUtils.hasText(login_captcha_code)){
            request.getSession().setAttribute("captchaCodeErrorMsg", "验证码错误!");
            // 重定向到登录页面
            response.sendRedirect("/login/toLogin");
        }
        
        if (!code.equalsIgnoreCase(login_captcha_code)){
            request.getSession().setAttribute("captchaCodeErrorMsg", "验证码输入错误!");
            // 重定向到登录页面
            response.sendRedirect("/login/toLogin");
        }
    
        //获取请求头中的Authorization
        String authorization = request.getHeader("Authorization");
        //如果Authorization为空,那么不允许用户访问,直接返回
        if (!StringUtils.hasText(authorization)) {
            printFront(response, "没有登录!");
            return;
        }
        //Authorization 去掉头部的Bearer 信息,获取token值
        String jwtToken = authorization.replace("Bearer ", "");
        //验签
        boolean verifyTokenResult = utilJWT.verifyToken(jwtToken);
        //验签不成功
        if (!verifyTokenResult) {
            printFront(response, "jwtToken 已过期");
            return;
        }
    
        //从payload中获取userInfo
        String userInfo = utilJWT.getUserInfo(jwtToken);
        //从payload中获取授权列表
        List<String> userAuth = utilJWT.getUserAuth(jwtToken);
        //创建登录用户
        SysUser sysUser = objectMapper.readValue(userInfo, SysUser.class);
        SecurityUser securityUser = new SecurityUser(sysUser);
        //设置权限
        List<SimpleGrantedAuthority> authList = userAuth.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        securityUser.setSimpleGrantedAuthorities(authList);
    
    
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToke = new UsernamePasswordAuthenticationToken(securityUser
              , null, authList);
        //通过安全上下文设置认证信息
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToke);
        //继续访问相应的rul等
        filterChain.doFilter(request, response);
        // 删除session中的验证码
        session.removeAttribute("code");
        this.doFilter(request,response,filterChain);
        
    
    }
    private void printFront(HttpServletResponse response, String message) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        HttpResult httpResult = new HttpResult();
        httpResult.setCode(-1);
        httpResult.setMsg(message);
        
        writer.print(objectMapper.writeValueAsString(httpResult));
        writer.flush();
    }
}

⑦:修改 web安全配置类WebSecurityConfig

java 复制代码
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Resource
    private JwtCheckFilter jwtCheckFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(jwtCheckFilter, UsernamePasswordAuthenticationFilter.class);
        http.formLogin().successHandler(myAuthenticationSuccessHandler).permitAll();
http.authorizeRequests()
        .mvcMatchers("/student/**").hasAnyAuthority("student:query","student:update")

                .anyRequest().authenticated(); //任何请求均需要认证(登录成功)才能访问
        http.csrf().disable();
        //禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

⑧:启动测试

相关推荐
嘉里蓝海3 小时前
我在嘉顺达蓝海的安全日常
安全
2301_780789663 小时前
渗透测试真的能发现系统漏洞吗
服务器·网络·安全·web安全·网络安全
嘉里蓝海3 小时前
我在嘉顺达蓝海的安全坚守
安全
你的人类朋友5 小时前
认识一下Bcrypt哈希算法
后端·安全·程序员
Coovally AI模型快速验证9 小时前
基于YOLO集成模型的无人机多光谱风电部件缺陷检测
人工智能·安全·yolo·目标跟踪·无人机
夏天的风9911 小时前
本地部署PLM系统,如何用 ZeroNews 实现远程访问?
安全·远程工作
wanhengidc11 小时前
高性价比云手机挑选指南
运维·网络·安全·游戏·智能手机
拉法豆粉13 小时前
三方软件测试可移植性测试哪些内容
数据库·安全
午夜游鱼15 小时前
Go 泛型实战:一行代码封装 sync.Pool,性能与安全兼得
开发语言·安全·golang
半桔17 小时前
【Linux手册】信号量与建造者模式:以 PV 操作保证并发安全,分步组装构建复杂对象
linux·运维·安全·建造者模式