Java-SpringBoot-2

学习视频:B站 狂神说Java – https://www.bilibili.com/video/BV1PE411i7CV

学习文档: 微信公众号 狂神说 –https://mp.weixin.qq.com/mp/homepage?__biz=Mzg2NTAzMTExNg==&hid=1&sn=3247dca1433a891523d9e4176c90c499&scene=18&uin=&key=&devicetype=Windows+10+x64&version=63020170&lang=zh_CN&ascene=7&fontgear=2

12、Spring Security

在web开发中,安全是第一位的。安全它是属于应用的非功能性要求, 过滤器、拦截器。在设计之初,就应该考虑进去。否则,如果是在应用开发的后期考虑安全问题的话,一方面,应用存在着严重的泄露问题,可能会泄露用户的隐私数据;另一方面,应用的基本架构已经确立了,如果修改的话,可能需要对系统架构进行很大的调整。这些都会增加开发周期性。所以,web开发的第一步就应该把安全相关因素考虑进去。

关于安全的框架有:shiro、Spring Security,完成的工作是用户认证(Authentication)和用户授权(Authorization)。

认证、授权(vip1,vip2,vip3),原先权限控制的有:

  • 功能权限
  • 访问权限
  • 菜单权限
  • 原来的过滤器、拦截器,大量的原生代码。冗余性

每种框架的出现都是针对某些问题。spring利用IOC 和AOP的思想,将对象创建的控制权由程序猿转交给第三方的IOC容器,低耦合性;面向切面编程AOP同样也是为了不影响原有代码的情况下,将一些通用的功能模块进行封装来切面开发。创建的目标都是为了:高内聚、低耦合,提高代码复用性,增加开发效率。

MVC、Spring、SpringBoot 这种的框架思想不外如是。

Spring Security:官方链接https://spring.io/projects/spring-security#overview

描述:

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。 它是保护基于 Spring 的应用程序的事实标准。
Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。 像所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以轻松扩展以满足自定义需求

从官网的介绍中可以知道,spring security是一种关于权限的框架。对功能权限,访问权限,和菜单权限等权限的控制。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。安全性主要是两个方面,用户认证和用户授权

  • 用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程
  • 用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

针对这两种问题,Spring Security 框架都有很好的支持。

  • 在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。
  • 在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。

demo:

实验环境搭建

新建一个springBoot项目,springboot-06-security

  • Maven依赖选择 web、 thymeleaf 模板引擎依赖

image-20220503111637960

image-20220503111756388

关闭模板引擎。application.properties:

# 关闭模板引擎
spring.thymeleaf.cache=false

导入静态资源:

[忘记这个资源来自哪里的博客了,不好意思。!!! 我提供下自己的百度网盘下载链接:链接:https://pan.baidu.com/s/1lRuPSFwb8oi33UR7NTzUUw?pwd=1234
提取码:1234】

image-20220503112423055

Controller跳转

写控制器进行跳转,横切的思想。

package com.al.Controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class RouterController {
    @RequestMapping({"/","/index"})
    public String index(){
        return "index";
    }

    @RequestMapping("/toLogin")
    public String toLogin(){
        return "views/login";
    }

    @RequestMapping("/level1/{id}")
    public String level1(@PathVariable("id") int id){
        return "views/level1/"+id;
    }

    @RequestMapping("/level2/{id}")
    public String level2(@PathVariable("id") int id){
        return "views/level2/"+id;
    }

    @RequestMapping("/level3/{id}")
    public String level3(@PathVariable("id") int id){
        return "views/level3/"+id;
    }
}

测试实验环境。启动程序进行测试:

image-20220503160345956

我们根据这个测试的结果,我们会让 vip1 2 3的访问权限不一样,或者让每个人进去之后,展现的界面不一样。 我们该如何去做?

用户认证与授权

认识Spring Security

Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!

记住几个类:

WebSecurityConfigurerAdapter:自定义Security策略

AuthenticationManagerBuilder:自定义认证策略

@EnableWebSecurity:开启WebSecurity模式

Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。

  • 认证”(Authentication)

身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。

身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。

  • “授权” (Authorization)

授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。

这个概念是通用的,而不是只在Spring Security 中存在。

我们的测试环境没有问题,任何人都可以访问。我们使用 Spring Security 去增加认证和授权的功能。

1、引入 Spring Security 模块

导入这个 security依赖:

        <!--security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

2、编写 Spring Security 配置类

参考官网:https://spring.io/projects/spring-security

查看我们自己项目中的版本,找到对应的帮助文档:

https://docs.spring.io/spring-security/reference/servlet/index.html

首页

首页 index.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>首页</title>
    <!--semantic-ui-->
    <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
    <link th:href="@{/qinjiang/css/qinstyle.css}" rel="stylesheet">
</head>
<body>

<!--主容器-->
<div class="ui container">

    <div class="ui segment" id="index-header-nav" th:fragment="nav-menu">
        <div class="ui secondary menu">
            <a class="item"  th:href="@{/index}">首页</a>

            <!--登录注销-->
            <div class="right menu">
                <!--未登录-->
                <div sec:authorize="!isAuthenticated()">
<!--                <a class="item" th:href="@{/toLogin}">-->
                <a class="item" th:href="@{/login}">
                    <i class="address card icon"></i> 登录
                </a>
                </div>
                <!--如果已登录-->
                <div sec:authorize="isAuthenticated()">
                    <a class="item">
                        <i class="address card icon"></i>
                        用户名:<span sec:authentication="principal.username"></span>
                        角色:<span sec:authentication="principal.authorities"></span>
                    </a>
                </div>
                <!--注销-->
                <div sec:authorize="isAuthenticated()">
                <a class="item" th:href="@{/logout}">
                    <i class="sign-out icon"></i> 注销
                </a>
                </div>
                <!--已登录
                <a th:href="@{/usr/toUserCenter}">
                    <i class="address card icon"></i> admin
                </a>
                -->
            </div>
        </div>
    </div>

    <div class="ui segment" style="text-align: center">
        <h3>Spring Security Study by Crazy God</h3>
    </div>

    <div>
        <br>
        <div class="ui three column stackable grid">
            <!--菜单根据用户的角色动态的实现. sec:authorize="hasRole('vip1')"-->
            <div class="column">
            <div class="column" sec:authorize="hasRole('vip1')">
<!--                <div class="ui raised segment">-->
                    <div class="ui">
                        <div class="content">
                            <h5 class="content">Level 1</h5>
                            <hr>
                            <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
                            <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
                            <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="column">
                <div class="ui raised segment" sec:authorize="hasRole('vip2')">
                    <div class="ui">
                        <div class="content">
                            <h5 class="content">Level 2</h5>
                            <hr>
                            <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
                            <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
                            <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="column">
                <div class="ui raised segment" sec:authorize="hasRole('vip3')">
                    <div class="ui">
                        <div class="content">
                            <h5 class="content">Level 3</h5>
                            <hr>
                            <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
                            <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
                            <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
                        </div>
                    </div>
                </div>
            </div>

        </div>
    </div>
    
</div>


<script th:src="@{/qinjiang/js/jquery-3.1.1.min.js}"></script>
<script th:src="@{/qinjiang/js/semantic.min.js}"></script>

</body>
</html>

用户授权

新建一个config文件夹,用于进行配置类文件。

创建 SecurityConfig ,需要继承 WebSecurityConfigurerAdapter。

package com.al.config;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

// AOP:拦截器
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // 链式编程
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 首页所有人可以访问, 功能页只有对应权限的人才访问
        // 请求授权的规则
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasRole("vip1")
                .antMatchers("/level2/**").hasRole("vip2")
                .antMatchers("/level3/**").hasRole("vip3");
    }
}

然后进行测试, 查看访问的结果。

发现除了首页都进不去了!因为我们目前没有登录的角色,因为请求需要登录的角色拥有对应的权限才可以

注意点: index.html 中的登录提交表单响应的url请求是 /login。就是spring security 默认的。原来如此啊。 后面解决问题的时候才发现这个问题。 th:href=“@{/toLogin}” 是为了自己写定制登录页面的。我说怎么一直不对劲

                <!--未登录-->
                <div sec:authorize="!isAuthenticated()">
<!--                <a class="item" th:href="@{/toLogin}">-->
                <a class="item" th:href="@{/login}">
                    <i class="address card icon"></i> 登录
                </a>
                </div>

添加 当用户没有权限进入指定页面时,让其跳转到登录页面:

  • 在configure()方法中加入以下配置,开启自动配置的登录功能!
    protected void configure(HttpSecurity http) throws Exception {
        // 首页所有人可以访问, 功能页只有对应权限的人才访问
        // 请求授权的规则
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasRole("vip1")
                .antMatchers("/level2/**").hasRole("vip2")
                .antMatchers("/level3/**").hasRole("vip3");

        // 开启自动配置的登录功能。没有权限默认会到登录界面,需要开启登录的页面
        // /login 请求来到登录页
        // /login?error 重定向到这里表示登录失败
        http.formLogin();
    }

测试,此时,点击首页的选项,会直接跳转到登录页面,让其进行登录。

但是这时候无法登录成功,不能进行登录。我们还需要进行配置。

  • 在上面的代码中我们完成的是授权,【授权某个用户的操作权限】
  • 接下来要去完成认证。【验证用户的合法主体,看这个用户能不能进去系统】

image-20220503202735178

用户认证

我们可以定义认证规则,重写configure(AuthenticationManagerBuilder auth)方法。

SecurityConfig 中的认证代码:

    // 定义认证规则
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //在内存中定义,也可以在jdbc中去拿.这些数据正常情况下应该从数据库中读
        auth.inMemoryAuthentication()
                .withUser("alzn").password("123456").roles("vip1","vip2","vip3")
                .and()
                .withUser("al").password("123456").roles("vip1","vip2")
                .and()
                .withUser("zn").password("123456").roles("vip3");
    }

运行测试,报错。 提示我们的密码没有加密:

控制台输出:java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null”

我们要将前端传过来的密码进行某种方式加密,否则就无法登录。修改代码:

    // 定义认证规则:springboot 2.1.x 还可以直接使用
    // 密码编码:PasswordEncoder
    // 在Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
    //要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
    //spring security 官方推荐的是使用bcrypt加密方式。
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //在内存中定义,也可以在jdbc中去拿.这些数据正常情况下应该从数据库中读
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("alzn").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
                .and()
                .withUser("al").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2")
                .and()
                .withUser("zn").password(new BCryptPasswordEncoder().encode("123456")).roles("vip3");
    }

运行测试,发现每个角色只能访问自己认证下的规则 的页面信息。

比如,alzn 可以访问所有的页面;zn只能访问 level3 即vip3的页面信息。

  • configure(AuthenticationManagerBuilder auth) 完成了用户认证和它的角色信息。从数据库中读取到用户的合法性,以及响应的角色权限信息
  • configure(HttpSecurity http) 完成了用户授权。根据用户的角色权限信息/某种权限 赋予其能够执行的操作。

验证了用户alzn的合法性,权限是vip1、vip2、vip3;授权vip1、vip2、vip3能够执行的操作。

注销及权限控制

注销

开启自动配置中的注销功能。

在定制请求的授权规则中,去开启注销功能。

    // 定制请求的授权规则
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 首页所有人可以访问, 功能页只有对应权限的人才访问
        // 请求授权的规则
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasRole("vip1")
                .antMatchers("/level2/**").hasRole("vip2")
                .antMatchers("/level3/**").hasRole("vip3");

        // 开启自动配置的登录功能。没有权限默认会到登录界面,需要开启登录的页面
        // /login 请求来到登录页
        // /login?error 重定向到这里表示登录失败
        http.formLogin();

        //注销,开启注销功能
        // /logout 注销请求
        http.logout();
    }

我们在前端,增加一个注销的按钮,index.html 导航栏中

                <!--注销-->
                <a class="item" th:href="@{/logout}">
                    <i class="sign-out icon"></i> 注销
                </a>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aw5k0URR-1651928369131)(…/…/…/…/…/Users/ALZN/AppData/Roaming/Typora/typora-user-images/image-20220503213251430.png)]

测试,我们在登录成功后去点击注销,发现注销完毕会跳转到登录页面!

image-20220503213447032

image-20220503213507112

跳转到登录页面:

image-20220503213543813

但是,我们想要==在注销成功之后跳转到首页==,那么如何去做呢?

**定制请求的授权规则 configure(HttpSecurity http) **中开启注销配置功能进行修改。如下所示:

  • .logoutSuccessUrl(“/”); 注销成功来到首页
        //注销,开启注销功能
        // /logout 注销请求
        //http.logout();
        http.logout().logoutSuccessUrl("/"); // .logoutSuccessUrl("/"); 注销成功来到首页

测试,在注销完毕之后能够成功跳转到首页 index.html 。

权限控制

我们现在又来一个需求:用户没有登录的时候,导航栏上只显示登录按钮,用户登录之后,导航栏可以显示登录的用户信息及注销按钮!还有就是,比如 al 这个用户,它只有 vip1,vip2功能,那么登录则只显示这两个功能,而vip3的功能菜单不显示!这个就是真实的网站情况了!该如何做呢?

我们需要结合thymeleaf中的一些功能

sec:authorize=“isAuthenticated()”:是否认证登录!来显示不同的页面

导入maven依赖:

<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity4 -->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

修改我们的前端页面:

导入命名空间:

xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"

image-20220504094301537

修改导航栏,增加认证判断。

index.html:

    <div class="ui segment" id="index-header-nav" th:fragment="nav-menu">
        <div class="ui secondary menu">
            <a class="item"  th:href="@{/index}">首页</a>

            <!--登录注销-->
            <div class="right menu">
                <!--未登录-->
                <div sec:authorize="!isAuthenticated()">
                <a class="item" th:href="@{/toLogin}">
                    <i class="address card icon"></i> 登录
                </a>
                </div>
                <!--如果已登录-->
                <div sec:authorize="isAuthenticated()">
                    <a class="item">
                        <i class="address card icon"></i>
                        用户名:<span sec:authentication="principal.username"></span>
                        角色:<span sec:authentication="principal.authorities"></span>
                    </a>
                </div>
                <!--注销-->
                <div sec:authorize="isAuthenticated()">
                <a class="item" th:href="@{/logout}">
                    <i class="sign-out icon"></i> 注销
                </a>
                </div>
                <!--已登录
                <a th:href="@{/usr/toUserCenter}">
                    <i class="address card icon"></i> admin
                </a>
                -->
            </div>
        </div>
    </div>

重启测试,我们可以登录试试看,登录成功后确实,显示了我们想要的页面;

如果注销404了,就是因为它默认防止csrf跨站请求伪造,因为会产生安全问题,我们可以将请求改为post表单提交,或者在spring security中关闭csrf功能;我们试试:在 配置中增加 http.csrf().disable();

http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.logout().logoutSuccessUrl("/");

我们继续将下面的角色功能块认证完成!

即vip1用户只显示vip1的界面,隐藏其它几个东西,如下所示:

image-20220504100202085

        <div class="ui three column stackable grid">
            <!--菜单根据用户的角色动态的实现. sec:authorize="hasRole('vip1')"-->
            <div class="column" sec:authorize="hasRole('vip1')">
                <div class="ui raised segment">
                    <div class="ui">
                        <div class="content">
                            <h5 class="content">Level 1</h5>
                            <hr>
                            <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
                            <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
                            <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="column">
                <div class="ui raised segment" sec:authorize="hasRole('vip2')">
                    <div class="ui">
                        <div class="content">
                            <h5 class="content">Level 2</h5>
                            <hr>
                            <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
                            <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
                            <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="column">
                <div class="ui raised segment" sec:authorize="hasRole('vip3')">
                    <div class="ui">
                        <div class="content">
                            <h5 class="content">Level 3</h5>
                            <hr>
                            <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
                            <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
                            <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
                        </div>
                    </div>
                </div>
            </div>

        </div>

测试。发现此时没有了登录,页面什么也没有。点击登录选项进行提交的时候也不行。

image-20220504102405246

查看代码。点击首页 index.html 中的登录选项,url请求是 th:href=“@{/toLogin}”,对应着控制器方法为:

    @RequestMapping("/toLogin")
    public String toLogin(){
        return "views/login";
    }

我在login.html页面进行分析:

image-20220504102729891

提交用户注册信息,url请求是 /usr/login。 我修改为 /login。

修改后,测试:用户 zn 的权限是 vip3,只显示 vip3权限对应的页面信息,level3

image-20220504102848616

总结下:

  • 用户认证

    • 控制器中使用了 Spring Security中的 configure(AuthenticationManagerBuilder auth) 去定义用户认证规则。对系统中的用户和相应权限的信息读取。

    • auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
              .withUser("alzn").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
      
    • 在前端页面,使用thymeleaf-extras-springsecurity5 去完成用户认证。sec:authorize="isAuthenticated() 进行读取认证用户的信息

  • 用户授权:

    • 在控制器中使用Spring Security中的configure(HttpSecurity http) 方法定义了 请求授权的规则。特定权限的用户只能访问相应页面或执行权限对应的操作。

    • // 请求授权的规则
      http.authorizeRequests()
              .antMatchers("/").permitAll()
              .antMatchers("/level1/**").hasRole("vip1")
      
    • 在前端页面给特定用户显示相应的页面信息。利用 thymeleaf 中thymeleaf-extras-springsecurity5 去进行用户角色的动态实现。前面已经利用sec:authorize="isAuthenticated() 读取认证用户的信息,此时再进行判断,完成显示相应权限的界面信息。

    • <div class="ui raised segment" sec:authorize="hasRole('vip3')">
          <div class="ui">
              <div class="content">
                  <h5 class="content">Level 3</h5>
      

记住我

现在的情况,我们只要登录之后,关闭浏览器,再登录,就会让我们重新登录,但是很多网站的情况,就是有一个记住密码的功能,这个该如何实现呢?

在用户授权那里 定义请求授权规则的方法中,添加 记住密码功能。

  • http.rememberMe(); //记住我
    // 链式编程
    // 定制请求的授权规则
    @Override
        protected void configure(HttpSecurity http) throws Exception {
        // 首页所有人可以访问, 功能页只有对应权限的人才访问
        // 请求授权的规则
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasRole("vip1")
                .antMatchers("/level2/**").hasRole("vip2")
                .antMatchers("/level3/**").hasRole("vip3");

        // 开启自动配置的登录功能。没有权限默认会到登录界面,需要开启登录的页面
        // /login 请求来到登录页
        // /login?error 重定向到这里表示登录失败
        http.formLogin();

        //注销,开启注销功能
        // /logout 注销请求
        //http.logout();
        http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
        http.logout().logoutSuccessUrl("/"); // .logoutSuccessUrl("/"); 注销成功来到首页

        // 记住我
        http.rememberMe();
    }

测试:发现登录页多了一个记住我功能,我们登录之后关闭 浏览器,然后重新打开浏览器访问,发现用户依旧存在!

但下面这个是我自己在 登录时故意输出密码才跳转到的登录页面。【我的登录选项中定义的url请求 /tologin,进行登录,不是这个。】

image-20220504105948085

针对上面这个,我出现的问题。我想着可以直接出现这个 Please sign in 登录界面。 我在 index.html前端页面进行了修改。直接使用 Spring Security 的 login 路径进行登录。

                <!--未登录-->
                <div sec:authorize="!isAuthenticated()">
<!--                <a class="item" th:href="@{/toLogin}">-->
                <a class="item" th:href="@{/login}">
                    <i class="address card icon"></i> 登录
                </a>

image-20220504110611452

此时测试,直接出现 login 登录页面,并增加了 记住我 的这一功能选项。

image-20220504110811340

登录后,关闭浏览器重新打开,仍然有此用户。

image-20220504111922836

查看浏览器的cookie:

  • 此时cookie,浏览器客户端进行了保存,默认有效时间是14天

image-20220504111250687

  • 我们点击注销的时候,可以发现,spring security 帮我们自动删除了这个 cookie

image-20220504111407823

结论:

  • 登录成功后,将cookie发送给浏览器保存,以后登录带上这个cookie,只要通过检查就可以免登录了。如果点击注销,则会删除这个cookie。

定制登录页

现在这个登录页面都是spring security 默认的,怎么样可以使用我们自己写的Login界面呢?

【前面写的时候,index.html 中的登录提交表单响应的url请求是 /login。就是spring security 默认的。原来如此啊。】

1、在刚才的登录页配置后面指定 loginpage

http.formLogin().loginPage("/toLogin");

2、然后前端也需要指向我们自己定义的 login请求:

index.html:

<a class="item" th:href="@{/toLogin}">
   <i class="address card icon"></i> 登录
</a>

3、在登录的时候,需要将这些信息发送到哪里。这些配置信息也需要我们进行配置,login.html 配置提交请求及方式,方式必须为post:

在 loginPage()源码中的注释上有写明:

<li>/authenticate GET - 登录表单</li>
  <li>/authenticate POST - 处理凭据,如果有效,则验证用户
  </li>
<li>/authenticate?error GET - 在此处重定向失败的身份验证尝试</li>
<li>/authenticate?logout GET - 成功登出后重定向到这里</li>
  </ul>

image-20220504130447227

在 login.html 页面的代码:

<form th:action="@{/login}" method="post">
   <div class="field">
       <label>Username</label>
       <div class="ui left icon input">
           <input type="text" placeholder="Username" name="username">
           <i class="user icon"></i>
       </div>
   </div>
   <div class="field">
       <label>Password</label>
       <div class="ui left icon input">
           <input type="password" name="password">
           <i class="lock icon"></i>
       </div>
   </div>
   <input type="submit" class="ui blue submit button"/>
</form>

4、这个请求提交上来,我们还需要验证处理,怎么做呢?我们可以查看formLogin()方法的源码!我们配置接收登录的用户名和密码的参数!

http.formLogin()
  .usernameParameter("username")
  .passwordParameter("password")
  .loginPage("/toLogin")
  .loginProcessingUrl("/login"); // 登陆表单提交请求

5、在登录页增加记住我的多选框

<input type="checkbox" name="remember"> 记住我

6、后端验证处理!

//定制记住我的参数!
http.rememberMe().rememberMeParameter("remember");

7、测试,OK

不必非要求登录表单请求 url路径是 /login 去进行登录。

前端定义我们的login请求:login.html:

image-20220504133343999

此时的SecurityConfig的代码为:

     //在登录页配置后面指定 loginpage
     //http.formLogin().loginPage("/toLogin");
     http.formLogin()
             .usernameParameter("username")
             .passwordParameter("password")
             .loginPage("/toLogin");

这时候进行访问测试:

image-20220504133536398

成功完成提交动作,进行用户权限对应的界面:

image-20220504133637623

假如在前端页面,我们非要让登录页面为 @{/login},那么我们对应的在配置页面进行更改:

http.formLogin()
     .usernameParameter("username")
     .passwordParameter("password")
     .loginPage("/toLogin")
     .loginProcessingUrl("/login"); // 登陆表单提交请求

image-20220504133850503

这时候,定义了登录表单请求 url,才能成功进行登录。

总体代码

SecurityConfig

关于Spring Security的配置代码:

package com.al.config;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

// AOP:拦截器
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // 链式编程
    // 定制请求的授权规则
    @Override
        protected void configure(HttpSecurity http) throws Exception {
        // 首页所有人可以访问, 功能页只有对应权限的人才访问
        // 请求授权的规则
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasRole("vip1")
                .antMatchers("/level2/**").hasRole("vip2")
                .antMatchers("/level3/**").hasRole("vip3");

        // 开启自动配置的登录功能。没有权限默认会到登录界面,需要开启登录的页面
        // /login 请求来到登录页
        // /login?error 重定向到这里表示登录失败
        //http.formLogin();

        //在登录页配置后面指定 loginpage
        //http.formLogin().loginPage("/toLogin");

        http.formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginPage("/toLogin")
                .loginProcessingUrl("/login"); // 登陆表单提交请求

        //注销,开启注销功能
        // /logout 注销请求
        //http.logout();
        http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
        http.logout().logoutSuccessUrl("/"); // .logoutSuccessUrl("/"); 注销成功来到首页

        // 记住我
        //http.rememberMe();
        //定制记住我的参数!
        http.rememberMe().rememberMeParameter("remember");
    }

    // 定义认证规则:springboot 2.1.x 还可以直接使用
    // 密码编码:PasswordEncoder
    // 在Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
    //要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
    //spring security 官方推荐的是使用bcrypt加密方式。
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //在内存中定义,也可以在jdbc中去拿.这些数据正常情况下应该从数据库中读
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("alzn").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
                .and()
                .withUser("al").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2")
                .and()
                .withUser("zn").password(new BCryptPasswordEncoder().encode("123456")).roles("vip3");
    }
}

前端代码

index.html:首页
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>首页</title>
    <!--semantic-ui-->
    <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
    <link th:href="@{/qinjiang/css/qinstyle.css}" rel="stylesheet">
</head>
<body>

<!--主容器-->
<div class="ui container">

    <div class="ui segment" id="index-header-nav" th:fragment="nav-menu">
        <div class="ui secondary menu">
            <a class="item"  th:href="@{/index}">首页</a>

            <!--登录注销-->
            <div class="right menu">
                <!--未登录-->
                <div sec:authorize="!isAuthenticated()">
                    <!-- 写自己 自定义登录页面:/toLogin-->
                    <a class="item" th:href="@{/toLogin}">
                    <!-- Spring Security自带的登录页面:/login-->
                    <!--<a class="item" th:href="@{/login}">-->
                    <i class="address card icon"></i> 登录
                </a>
                </div>
                <!--如果已登录-->
                <div sec:authorize="isAuthenticated()">
                    <a class="item">
                        <i class="address card icon"></i>
                        用户名:<span sec:authentication="principal.username"></span>
                        角色:<span sec:authentication="principal.authorities"></span>
                    </a>
                </div>
                <!--注销-->
                <div sec:authorize="isAuthenticated()">
                <a class="item" th:href="@{/logout}">
                    <i class="sign-out icon"></i> 注销
                </a>
                </div>
                <!--已登录
                <a th:href="@{/usr/toUserCenter}">
                    <i class="address card icon"></i> admin
                </a>
                -->
            </div>
        </div>
    </div>

    <div class="ui segment" style="text-align: center">
        <h3>Spring Security Study by Crazy God</h3>
    </div>
    <div>
        <br>
        <div class="ui three column stackable grid">
            <!--菜单根据用户的角色动态的实现. sec:authorize="hasRole('vip1')"-->
            <div class="column">
            <div class="column" sec:authorize="hasRole('vip1')">
<!--                <div class="ui raised segment">-->
                    <div class="ui">
                        <div class="content">
                            <h5 class="content">Level 1</h5>
                            <hr>
                            <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
                            <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
                            <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="column">
                <div class="ui raised segment" sec:authorize="hasRole('vip2')">
                    <div class="ui">
                        <div class="content">
                            <h5 class="content">Level 2</h5>
                            <hr>
                            <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
                            <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
                            <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="column">
                <div class="ui raised segment" sec:authorize="hasRole('vip3')">
                    <div class="ui">
                        <div class="content">
                            <h5 class="content">Level 3</h5>
                            <hr>
                            <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
                            <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
                            <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<script th:src="@{/qinjiang/js/jquery-3.1.1.min.js}"></script>
<script th:src="@{/qinjiang/js/semantic.min.js}"></script>
</body>
</html>
login.html:定制登录页面
    <div class="ui segment">

        <div style="text-align: center">
            <h1 class="header">登录</h1>
        </div>

        <div class="ui placeholder segment">
            <div class="ui column very relaxed stackable grid">
                <div class="column">
                    <div class="ui form">
<!--                        <form th:action="@{/usr/login}" method="post">-->
                        <form th:action="@{/login}" method="post">
                            <div class="field">
                                <label>Username</label>
                                <div class="ui left icon input">
                                    <input type="text" placeholder="Username" name="username">
                                    <i class="user icon"></i>
                                </div>
                            </div>
                            <div class="field">
                                <label>Password</label>
                                <div class="ui left icon input">
                                    <input type="password" name="password">
                                    <i class="lock icon"></i>
                                </div>
                            </div>
                            <div class="field">
                                <input type="checkbox" name="remember"> 记住我
                            </div>
                            <input type="submit" class="ui blue submit button"/>
                        </form>
                    </div>
                </div>
            </div>
        </div>

13、Apache Shiro

参考博客链接:https://bareth.blog.csdn.net/article/details/109492486

Shiro简介

什么是Shiro?

Apache Shiro是一个功能强大且易于使用的Java安全框架用于执行身份验证,授权,加密和会话管理

使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序-从最小的移动应用程序到最大的Web和企业应用程序。【不仅可以用在JavaSE环境,也可以用在JavaEE环境】

Shiro的官网:https://shiro.apache.org/

原文链接:https://blog.csdn.net/q1105441883/article/details/117411764

  • JavaSE:指 Java Standard Edition,Java 标准版。是整个JAVA的基础和核心,也是 Java EE和 Java ME 技术的基础,主要用于开发桌面应用程序。

    它允许开发和部署在桌面、服务器、嵌入式环境和实时环境中使用的 Java 应用程序。

    Java SE 包含了支持 Java Web 服务开发的类,并为 Java Platform,Enterprise Edition(Java EE)提供基础。

  • JavaEE:Java Enterprise Edition,Java 企业版,多用于企业级开发,包括 web 开发等等。也叫J2EE。它提供了企业级应用开发的完整解决方案,比如开发网站,还有企业的一些应用系统,是JAVA技术应用最广泛的领域。企业版本帮助开发和部署可移植、健壮、可伸缩且安全服务器端 Java 应用程序。

    Java EE 是在 Java SE 的基础上构建的,它提供 Web 服务、组件模型、管理和通信 API,可以用来实现企业级的面向服务体系结构(service-oriented architecture,SOA)和 Web 2.0 应用程序。

  • JavaME:Java Micro Edition,它叫做 JAVA 的微缩版,主要应用于嵌入式开发,比如手机程序的开发。 Java ME 为在移动设备和嵌入式设备(比如手机、PDA、电视机顶盒和打印机)上运行的应用程序提供一个健壮且灵活的环境。 Java ME 包括灵活的用户界面、健壮的安全模型、许多内置的网络协议以及对可以动态下载的连网和离线应用程序的丰富支持。

    基于 Java ME 规范的应用程序只需编写一次,就可以用于许多设备,而且可以利用每个设备的本机功能。

    ————————————

原文链接:https://blog.csdn.net/qq_41566219/article/details/103976189

  • JavaSE包含的内容:

    • 面向对象

    • 多线程

    • IO流

    • JavaSwing等… 【Swing 是一个为Java设计的GUI工具包。是JAVA基础类的一部分。

      Swing包括了图形用户界面(GUI)器件如:文本框,按钮,分隔窗格和表。】

  • JavaEE的内容:

    • servlet
    • jstl
    • jsp
    • spring
    • mybatis
  • JavaSE、JavaEE、JavaME三者之间的关系:

    img

Shiro的功能有哪些?

关于图片的链接出处没有找到。…

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EvZ7Tri0-1651928577705)(…/…/…/…/…/Users/ALZN/AppData/Roaming/Typora/typora-user-images/image-20220504204257347.png)]

  • Authentication :身份认证、登录,验证用户是不是拥有相应的身份,是不是当前的系统的合法用户。

  • Authorization: 授权,权限验证。授予某个认证的用户能够执行他所拥有的权限下的操作。如:验证某个用户是否拥有某个角色,或者细粒度的验证某个用户对某个资源是否具有某个权限。

  • session Manager: 会话管理,即用户登录后就是一次会话。在没有退出之前,它的所有信息都在会话中。

    会话可以是普通的JavaSE环境,也可以是Web环境。

  • Cryptography:加密,保护数据的安全性。比如密码加密存储到数据库中,而不是明文存储

  • Web Support:web支持,可以非常容易的集成web环境

  • caching:缓存。比如用户登录后,该用户信息,拥有的角色、权限不用每次都去查询,这样可以提高效率。

  • concurrency: Shiro支持多线程应用的并发验证。比如:可以在一个线程中开启另一个线程,能够自动的把权限传送过去。

  • Testing:提供测试支持

  • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。

  • Remember Me:记住我。即一次登录后,下次再来的话就不用登录了。

Shiro架构

资料链接来源:https://www.cnblogs.com/flyuphigh/p/8058454.html

从外部看: 从应用程序的角度查看如何使用shiro完成工作:

img

可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;其每个API的含义:

Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者

SecurityManager安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器

Realm:域,Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。【Realm中拥有用户、角色、权限等安全数据】

也就是说对于我们而言,最简单的一个Shiro应用:

1、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;

2、我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。

从以上也可以看出,Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入。

从Shiro内部来看下Shiro的架构,如下图所示:

img

Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;

SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。

Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;

Authorizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;

Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;【pluggable realms:JDBC、Custom】

SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);

SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;

CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能

Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。

环境搭建

Shiro的官网:https://shiro.apache.org/ Github:https://github.com/apache/shiro

创建一个普通的 Maven项目, springboot-08-shiro:

删除掉 src文件, 创建一个 module, hello-shiro。

我们从 shiro的官网中的github网址中下载文件, 然后看其中的例子 samples->quikstart 文件中的 pom.xml 配置文件: 然后将maven依赖 复制到我们的项目中:

  • 官网下载链接:https://shiro.apache.org/download.html
  • 下载源文件:Source Code Distribution

pom.xml Maven配置信息:

    <dependencies>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.9.0</version>
        </dependency>

        <!-- configure logging -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.17.2</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.17.2</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

静态资源 resource中的 log4j.properties 配置文件的导入:

log4j.rootLogger=INFO, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

# General Apache libraries
log4j.logger.org.apache=WARN

# Spring
log4j.logger.org.springframework=WARN

# Default Shiro logging
log4j.logger.org.apache.shiro=INFO

# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN

将quickstart文件路径下静态资源 resources 中的 shiro.ini 配置文件导入到 hello-shiro 中。

shiro.ini:

[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
# 
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5

再将 Quickstart.java 文件导入到 hello-shiro中,启动运行:

Quickstart报错,查了好久,发现是factory方法过时了,新版本会有异常。

将Quickstart.java中过时的这两行代码进行修改:

        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();

image-20220505202203865

修改成如下所示:

DefaultSecurityManager securityManager = new DefaultSecurityManager();
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
securityManager.setRealm(iniRealm);

image-20220505202433209

修改之后,Quickstart.java代码能够成功运行。

出现错误:

Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/beanutils/ConvertUtilsBean
	at org.apache.shiro.config.IniSecurityManagerFactory.<init>(IniSecurityManagerFactory.java:64)
	at org.apache.shiro.config.IniSecurityManagerFactory.<init>(IniSecurityManagerFactory.java:68)
	at org.apache.shiro.config.IniSecurityManagerFactory.<init>(IniSecurityManagerFactory.java:73)
	at Quickstart.main(Quickstart.java:50)
Caused by: java.lang.ClassNotFoundException: org.apache.commons.beanutils.ConvertUtilsBean
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)

没有这样的类定义错误。在maven依赖可以发现,这样的 jar包爆红,并不存在。

image-20220505201857379

Shiro的Subject分析

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Quickstart {

    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


    public static void main(String[] args) {
        // Use the shiro.ini file at the root of the classpath
        // (file: and url: prefixes load from files and urls respectively):
        //Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        //SecurityManager securityManager = factory.getInstance();
        // 针对上方过时的代码,解决的方案:
        // 定义的 安全管理器securityManager ,管理所有的subject;所有与安全有关的操作都与securityManager进行交互
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
        securityManager.setRealm(iniRealm); // 获取安全数据源,如用户、角色、权限等

        SecurityUtils.setSecurityManager(securityManager);

        // Now that a simple Shiro environment is set up, let's see what you can do:

        // get the currently executing user:
        // 获取当前的用户对象 Subject
        Subject currentUser = SecurityUtils.getSubject();

        // Do some stuff with a Session (no need for a web or EJB container!!!)
        // 通过当前用户 拿到session
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("Retrieved the correct value! [" + value + "]"); //测试输出的内容,即输出 session信息
        }

        // let's login the current user so we can check against roles and permissions:
        // 如果当前用户没有被验证
        if (!currentUser.isAuthenticated()) {
            // Token:令牌
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
            token.setRememberMe(true); //设置 请记住我
            try {
                currentUser.login(token); //去执行登录操作
            } catch (UnknownAccountException uae) {
                log.info("There is no user with username of " + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {
                log.info("Password for account " + token.getPrincipal() + " was incorrect!");
            } catch (LockedAccountException lae) {
                log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                        "Please contact your administrator to unlock it.");
            }
            // ... catch more exceptions here (maybe custom ones specific to your application?
            catch (AuthenticationException ae) {
                //unexpected condition?  error?
            }
        }

        //say who they are:
        //print their identifying principal (in this case, a username):
        log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

        //test a role:
        if (currentUser.hasRole("schwartz")) {
            log.info("May the Schwartz be with you!");
        } else {
            log.info("Hello, mere mortal.");
        }

        //test a typed permission (not instance-level)
        //粗粒度
        if (currentUser.isPermitted("lightsaber:wield")) {
            log.info("You may use a lightsaber ring.  Use it wisely.");
        } else {
            log.info("Sorry, lightsaber rings are for schwartz masters only.");
        }

        //a (very powerful) Instance Level permission:
        // 细粒度
        if (currentUser.isPermitted("winnebago:drive:eagle5")) {
            log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  " +
                    "Here are the keys - have fun!");
        } else {
            log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
        }

        //all done - log out!
        // 注销
        currentUser.logout();

        // 结束
        System.exit(0);
    }
}

在这里,获取用户的角色、权限。进行输出测试。

  • 在shiro.ini配置文件中,关于用户 lonestarr:角色、权限
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz
  • 在Quickstart.java文件中获取该用户 lonestarr,并测试输出此用户的角色信息和相应的权限
...
// 如果当前用户没有被验证
        if (!currentUser.isAuthenticated()) {
            // Token:令牌
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
            token.setRememberMe(true); //设置 请记住我
            try {
                ...

此时测试输出的结果是:

21:06:51.100 [main] INFO  Quickstart - Subject=>session [aValue]
21:06:51.110 [main] INFO  Quickstart - User [lonestarr] logged in successfully.
21:06:51.111 [main] INFO  Quickstart - May the Schwartz be with you!
21:06:51.112 [main] INFO  Quickstart - You may use a lightsaber ring.  Use it wisely.
21:06:51.113 [main] INFO  Quickstart - You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  Here are the keys - have fun!

如果将用户修改为:

UsernamePasswordToken token = new UsernamePasswordToken("lonestarr222", "vespa");

测试输出结果:

20:53:39.256 [main] INFO  Quickstart - Subject=>session [aValue]
20:53:39.263 [main] INFO  Quickstart - There is no user with username of lonestarr222
20:53:39.264 [main] INFO  Quickstart - User [null] logged in successfully.
20:53:39.264 [main] INFO  Quickstart - Hello, mere mortal.
20:53:39.264 [main] INFO  Quickstart - Sorry, lightsaber rings are for schwartz masters only.
20:53:39.264 [main] INFO  Quickstart - Sorry, you aren't allowed to drive the 'eagle5' winnebago!

Shiro中Subject的方法

shiro中用户 subject的方法有:

Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
currentUser.isAuthenticated()
currentUser.getPrincipal()
currentUser.hasRole("schwartz")
currentUser.isPermitted("lightsaber:wield")
currentUser.logout();

SpringBoot整合Shiro

环境搭建

在 springboot-07-shiro项目中新建一个模块 shiro-springboot。选择spring initializ, 并选择 web依赖、thymeleaf依赖。此时的pom.xml maven配置依赖:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</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>
        </dependency>
    </dependencies>
Controller和首页

编写首页及其Controller:

在控制器包 controller 中创建控制器 MyController。编写关于首页跳转的controller:

  • @Controller 用于模板引擎thymeleaf中的跳转templates文件夹中的 页面信息
  • Model 用于设置传递给前端的参数信息
package com.al.shirospringboot.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class MyController {
    @RequestMapping({"/", "/index"})
    public String toIndex(Model model){
        model.addAttribute("msg", "hello Shiro");
        return "index";
    }
}

在templates 目录文件夹下新建首页 index.html。

  • 需要导入 thymeleaf 的命名空间
  • 前端的语法,获取参数时,表达时要使用 thymeleaf语法。这里使用 th:text 获取后端传递给前端的msg 参数
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>首页</h1>
<p th:text="${msg}"></p>
</body>
</html>

然后进行测试。 浏览器输入 url,成功访问后,代表 shiro 环境没有问题。

导入shiro-spring依赖

pom.xml中关于 shiro-spring 的依赖;

        <!--
        subject:主体
        SecurityManager:管理所有用户的操作,相当于DispatchServlet
        Realm:连接数据。安全数据源,存放的是用户、角色、权限信息
        -->
        <!--shiro整合spring的包-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.9.0</version>
        </dependency>
编写Shiro配置类

前面已经了解到 从外部 application 的角度看 shiro架构分为三个部分:【在Quickstart.java中也了解,验证了这点】

  • Subject
  • SecurityManager
  • Realm

最简单的一个Shiro应用:

1、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;

2、我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。

在主程序同级目录下新建config包,添加新建ShiroConfig配置类。在这里配置三大对象并将其注入到spring容器中:

  • realm对象:可看作安全实体的数据源,该对象需要自定义,继承AuthorizingRealm类

  • DefaultWebSecurityManager对象:默认安全管理器实体

  • ShiroFilterFactoryBean对象:Shiro过滤工厂实体

按照需要定义的那三个类进行定义:

  • 先创建 realm对象,创建一个 UserRealm 类。对象 realm 需要继承AuthorizingRealm:重写授权和认证的方法

    package com.al.shirospringboot.config;
    
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    
    // 自定义Realm
    public class UserRealm extends AuthorizingRealm {
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            System.out.println("执行了=>授权doGetAuthorizationInfo");
            return null;
        }
    
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            System.out.println("执行了=>认证doGetAuthenticationInfo");
            return null;
        }
    }
    

写我们的配置类 ShiroConfig:声明三个对象:

  • realm安全实体数据源:用我们自定义的UserRealm类来创建
  • DefaultWebSecurityManager默认安全管理器:该对象需要关联realm对象,在方法参数中传入realm对象的参数,用@Qualifier指定需要的realm实现类UserRealm方法名即可【需获取 realm 对象信息】
  • ShiroFilterFactoryBeanshiro过滤工厂对象:该对象需要关联SecurityManager对象,同样在参数中传入该对象的参数,用@Qualifier指定需要的实现类方法名

ShiroConfig:

package com.al.shirospringboot.config;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroConfig {
    //创建realm对象,自定义
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }

    //DefaultWebSecurityManager
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // securityManager 安全管理器关联 Realm (userRealm)
        securityManager.setRealm(userRealm);
        return securityManager;
    }
    //ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);
        return bean;
    }
}

Shiro实现登录与拦截

编写Controller和页面

想要完成的功能:

  • 点击add即可跳转到add.html,点击update即可跳转到update.html。

控制器 MyController:

  • 添加用户控制器: add
  • 更新修改用户控制器:update
    @RequestMapping("/user/add")
    public String add() {
        return "user/add";
    }

    @RequestMapping("/user/update")
    public String update() {
        return "user/update";
    }

编写前端页面:

  • 在templates目录下新建user包,编写add.html和update.html页面
  • 在add.html 和 update.html页面信息
  • 在首页上增加相应跳转的链接【add选项的url请求/ upadate 选项的请求链接】

image-20220506172415557

add.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>add</title>
</head>
<body>
<h1>add</h1>
</body>
</html>

update.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>update</title>
</head>
<body>
<h1>update</h1>
</body>
</html>

index.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>首页</h1>
<p th:text="${msg}"></p>
<hr>
<a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">update</a>
</body>
</html>
实现登录拦截

我们想要只有登录的用户合法,存在时,才能够 点击add即可跳转到add.html,点击update即可跳转到update.html。

这就需要使用拦截器完成过滤的功能,跳转到各自需要 只能显示的界面。

在shiro配置类中,我们创建了三个对象。

  • 如果要实现登录拦截功能,就要用到shiro过滤工厂对象 ShiroFilterFactoryBean
  • 我们在配置类ShiroConfigshiroFilterFactoryBean()方法中添加shiro的拦截器,实现登录过滤的功能

在shiroFilterFactoryBean()方法中设置拦截器setFilterChainDefinitionMap:

    //ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);
        // 添加shiro内置的过滤器
            /*
            anon:无需认证就可以访问
            authc:必须认证了才能访问
            user:必须拥有记住我功能才能使用
            perms:拥有对某个资源的权限才能访问
            role:拥有某个角色权限才能访问
         */
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/user/add","authc");
        filterMap.put("/user/update","authc");
        bean.setFilterChainDefinitionMap(filterMap);
        return bean;
    }

测试:已经能完成过滤 拦截器的功能,点击add 或者 update 无法跳转,说明已经拦截了。

我们还需要去添加一个 登录链接,让点击add 无法访问的去跳转到登录页面。

  • 编写login.html 登录页面:

    <!DOCTYPE html>
    <html lang="en">
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>登录</title>
    </head>
    <body>
    <h1>登录</h1>
    <form action="/login">
        <p>用户名:<input type="text" name="username"></p>
        <p>密码:<input type="text" name="password"></p>
        <p><input type="submit"></p>
    </form>
    </body>
    </html>
    
  • 编写登录页面请求 对应的控制器,对应的url请求链接为 /login

    @RequestMapping("/login")
    public String login() {
        return "login";
    }
    
  • 和Spring Security学习时一样,同样在用户认证授权时,对于不是合法的用户,我们让其跳转到登录页面。在用户授权处设置登录链接。

    Spring Security中的用户授权规则:

    // 定制请求的授权规则
     @Override
         protected void configure(HttpSecurity http) throws Exception {
         // 请求授权的规则
         http.authorizeRequests()
                 .antMatchers("/").permitAll()
                 .antMatchers("/level1/**").hasRole("vip1")
                 .antMatchers("/level2/**").hasRole("vip2")
                 .antMatchers("/level3/**").hasRole("vip3");
         // 开启自动配置的登录功能。没有权限默认会到登录界面,需要开启登录的页面
         // /login 请求来到登录页
         // /login?error 重定向到这里表示登录失败
         //http.formLogin();
         //在登录页配置后面指定 loginpage
         //http.formLogin().loginPage("/toLogin");
         http.formLogin()
                 .usernameParameter("username")
                 .passwordParameter("password")
                 .loginPage("/toLogin")
                 .loginProcessingUrl("/login"); // 登陆表单提交请求
    
         //注销,开启注销功能
         // /logout 注销请求
         //http.logout();
         http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
         http.logout().logoutSuccessUrl("/"); // .logoutSuccessUrl("/"); 注销成功来到首页
         // 记住我
         //http.rememberMe();
         //定制记住我的参数!
         http.rememberMe().rememberMeParameter("remember");
     }
    

在这里,对shiro过滤工厂对象 ShiroFilterFactoryBean,去添加设置登录的请求:

  • 在这里使用通配符: filterMap.put(“/user/*”, “authc”); 【针对所有的user/下的请求过滤处理】
    //ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);
        // 添加shiro内置的过滤器
            /*
            anon:无需认证就可以访问
            authc:必须认证了才能访问
            user:必须拥有记住我功能才能使用
            perms:拥有对某个资源的权限才能访问
            role:拥有某个角色权限才能访问
         */
        Map<String, String> filterMap = new LinkedHashMap<>();
        //filterMap.put("/user/add","authc");
        //filterMap.put("/user/update","authc");
        filterMap.put("/user/*", "authc"); //支持通配符
        bean.setFilterChainDefinitionMap(filterMap);

        //设置登录的请求
        bean.setLoginUrl("/login");
        return bean;
    }

测试一下,访问localhost:8080。点击add、update无法正常访问,跳转到如下图所示的登录界面。这是因为拦截器的作用。/user/add请求,以及/user/authc请求需要认证才能访问。

image-20220506193135603

如果设置成如下所示的用户角色权限:

    filterMap.put("/user/add", "anon");
    filterMap.put("/user/update", "authc");

此时进行测试,访问localhost:8080。

  • 点击首页中的 add可以正常访问,
  • 点击update无法正常访问,跳转到登录页面。这是因为/user/authc请求需要认证才能访问

Shiro实现用户认证

在前面创建的关于 Realm对象中,它继承了AuthorizingRealm类。重写了其中的两个方法:

  • doGetAuthorizationInfo:授权
  • doGetAuthenticationInfo:认证

关于Shiro实现用户认证功能,我们在这里进行实现。【doGetAuthenticationInfo完成用户认证,和前面的Quickstart.java中的代码类似】。实现用户认证的步骤:

  1. 获取当前用户
  2. 将当前用户的信息封装生成 token 令牌
  3. 执行登录操作
Controller获取用户session

我们需要获取用户的session。在这里,用户的session为名称和密码:

  • username
  • password

前端把登录用户的信息 session参数传递给后端控制器 controller 中。我们在MyController 控制器中添加 toLogin()方法。

MyController

    @RequestMapping("/toLogin")
    public String toLogin(String username, String password, Model model) {
        //获取当前用户
        Subject subject = SecurityUtils.getSubject();
        //封装用户信息生成token令牌
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        //执行登录操作,可以自定义捕获异常
        try {
            subject.login(token); // 执行登录方法
            return "index";//登录成功返回首页
        } catch (UnknownAccountException e) {
            model.addAttribute("msg", "用户名不存在");
            return "login";//用户名错误回到登录页面
        } catch (IncorrectCredentialsException e) {
            model.addAttribute("msg", "密码不正确");
            return "login";//证书
        }
    }
login.html页面完善

前端页面的登录功能选项的跳转:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h1>登录</h1>
<p th:text="${msg}" style="color: red"></p>
<!--<form action="/login">-->
<form action="/toLogin">
    <p>用户名:<input type="text" name="username"></p>
    <p>密码:<input type="text" name="password"></p>
    <p><input type="submit"></p>
</form>
</body>
</html>

测试:此时启动程序,进行访问:

image-20220506195621242

此时进行登录的时候,报错。显示用户名不存在。而在控制台输出了:

执行了=>认证doGetAuthenticationInfo

这表明,在用户登录的时候会默认执行 用户认证的方法。我们需要在该方法中添加认证用户的信息。正如从外部看Shiro的架构信息:Subject:用户主体;SecurityManager是负责进行交互,执行用户权限的操作,相当于DispatchServlet;Realm相当于安全数据源,里面有用户角色、权限信息。

SecurityManager需要从 Realm 中获取用户角色、权限信息,来判断用户是否合法,能够执行哪种权限的操作

所以我们需要在Realm对象中定义用户的认证信息。

  • 需要return null,就会自动抛出UnknownAccountException异常

  • MyController控制器中捕获该异常,

    catch (UnknownAccountException e) {
    model.addAttribute(“msg”, “用户名不存在”);
    return “login”;//用户名错误回到登录页面

    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了=>认证doGetAuthenticationInfo");
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        //伪造正确用户名和密码
        String username = "alzn";
        String password = "123456";
        //用户名认证
        if (!userToken.getUsername().equals(username))
            return null;//只需要return null,就会自动抛出UnknownAccountException异常
        //密码认证,涉及到安全问题,shiro自动完成
        return new SimpleAuthenticationInfo("", password, "zsr");
    }

重新启动主程序进行测试,结果表明 只有当 用户名和密码都正确时,才能正常访问。

用户登录认证完成后, 还需要进行授权认证的功能实现。

Shiro整合Mybatis

前面已经实现了用户认证。但在真实的开发中,所有的用户信息都在数据库中,因此现在来整合数据库进行使用。连接数据库后,完成授权的功能。

导入maven依赖,在这里我们使用druid数据源。依赖:

  • mysql连接驱动
  • druid数据源
  • log4j 日志,配合druid数据源使用
  • lombok。简化开发

pom.xml:

<!--MySQL连接驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!--Druid数据源-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.1</version>
</dependency>
<!--log4j-->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.12</version>
</dependency>
<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
</dependency>
<!--springboot-mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>

数据库的创建和连接

创建数据库 shiro_mybatis:

-- 创建数据库
CREATE DATABASE shiro_mybatis;
-- 使用shiro_myabtis数据库
use shiro_mybatis;

-- 创建user表
CREATE TABLE IF NOT EXISTS `user`(
	`id` INT(4) NOT NULL AUTO_INCREMENT COMMENT '身份号',
	`name` VARCHAR(30) NOT NULL DEFAULT '匿名' COMMENT '姓名',
	`pwd` VARCHAR(30) NOT NULL DEFAULT '123456' COMMENT '密码',
	PRIMARY KEY (`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8

-- 给user表插入数据
INSERT INTO `user`(`id`,`name`,`pwd`) 
VALUES ('1','alzn','123456'),('2','al','654321'),('3','zn','20082022');

在springboot项目中,数据库资源配置文件里面,我们将数据库源默认的修改为 Druid 。资源配置文件 application.yaml:

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/shiro_mybatis?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    #Spring Boot 默认是不注入这些属性值的,需要自己绑定
    #druid 数据源专有配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

    #配置监控统计拦截的filters
    # stat:监控统计
    # log4j:日志记录(需要导入log4j依赖)
    # wall:防御sql注入
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

在Test中创建测试类进行测试。@SpringBootApplication 注解,Spring Boot完成自动配置。

package com.al.shirospringboot;

import com.alibaba.druid.pool.DruidDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@SpringBootTest
class ShiroSpringbootApplicationTests {

    // DI 注入数据源。从spring IOC 容器中获取数据配置信息
    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {
        // 查看数据源: 
        System.out.println(dataSource.getClass());
        // 获得数据库连接:
        Connection connection = dataSource.getConnection();
        System.out.println(connection); 
        DruidDataSource druidDataSource = (DruidDataSource) dataSource;
        System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());
        System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize());

        connection.close(); // 关闭连接
    }

}

测试发现,可以成功连接数据源。

pojo实体类

创建实体类 User,和数据库表user对应。

package com.al.shirospringboot.pojo;

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

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;
    private String name;
    private String pwd;
}
Mapper层

编写mapeer层,对底层数据库进行操作。创建mapper包,新建UserMapper接口:

package com.al.shirospringboot.mapper;

import com.al.shirospringboot.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

@Mapper //表示这是Mybatis的mapper类
@Repository
public interface UserMapper {
    //通过用户名查询用户
    User queryUserByName(String username);
}

mapper接口实现类

image-20220506212745932

在resources文件下创建一个 mapper 资源文件,新建UserMapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.al.shirospringboot.mapper.UserMapper">
    <select id="queryUserByName" parameterType="String" resultType="com.al.shirospringboot.pojo.User">
        select * from shiro_mybatis.user where name=#{name};
    </select>
</mapper>

yaml资源配置文件中关于pojo和mapper的绑定。核心配置文件中绑定该UserMapper.xml文件:

#绑定mapper.xml
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.al.shirospringboot.pojo

如果是 properties 资源配置文件,则绑定方式为:

# 整合mybatis
mybatis.type-aliases-package=com.al.pojo
mybatis.mapper-locations=classpath:mybatis/dao/*.xml
Service层

关于service包下新建UserServiceUserServiceImpl两个类。

UserService:

package com.al.shirospringboot.service;

import com.al.shirospringboot.pojo.User;

public interface UserService {
    public User queryUserByName(String username);
}

UserServiceImpl:

package com.al.shirospringboot.service;

import com.al.shirospringboot.Mapper.UserMapper;
import com.al.shirospringboot.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;
    @Override
    public User queryUserByName(String username) {
        return null;
    }
}
测试

在springboot提供的测试类中进行测试,根据用户名查询用户。

package com.al.shirospringboot;

import com.al.shirospringboot.service.UserServiceImpl;
import com.alibaba.druid.pool.DruidDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@SpringBootTest
class ShiroSpringbootApplicationTests {
    @Autowired
    UserServiceImpl userService;

    @Test
    void testGetUser() {
        System.out.println(userService.queryUserByName("alzn"));
    }
}

测试结果,能够输出用户信息:

User(id=1, name=alzn, pwd=123456)
Realm获取数据库用户信息

我们把原来伪造的用户信息使用数据库来代替。我们修改UserRealm中的代码。Realm中拥有用户的角色、权限信息。SecurityManager从Realm中获取这些认证用户权限执行相应的操作。

  • 从Spring容器中获取注入的UserServiceImpl 对象
  • 利用userService 获取数据库中的对象,将其生成token 放在realm中

UserRealm 中的代码:

package com.al.shirospringboot.config;

import com.al.shirospringboot.pojo.User;
import com.al.shirospringboot.service.UserServiceImpl;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

// 自定义Realm
public class UserRealm extends AuthorizingRealm {
    @Autowired
    UserServiceImpl userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("执行了=>授权doGetAuthorizationInfo");
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了=>认证doGetAuthenticationInfo");
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        /**
        //伪造正确用户名和密码
        String username = "alzn";
        String password = "123456";
        //用户名认证
        if (!userToken.getUsername().equals(username))
            return null;//只需要return null,就会自动抛出UnknownAccountException异常
        //密码认证,涉及到安全问题,shiro自动完成
        return new SimpleAuthenticationInfo("", password, "zsr");
         */
        //连接真实的数据库
        User user = userService.queryUserByName(userToken.getUsername());
        //用户名认证
        if (user == null)
            return null;//只需要return null,就会自动抛出UnknownAccountException一场
        //密码认证,涉及到安全问题,shiro自动完成
        return new SimpleAuthenticationInfo("", user.getPwd(), "");
    }
}

测试:只要输入数据库中正确的用户名和密码即可实现登录。

Shiro请求授权

添加授权规则

要去实现登录拦截(判断权限,请求权限规则),同样通过shiro过滤工厂设置权限实现。

在我们的关于shiro配置类 ShiroConfig 中的 ShiroFilterFactoryBean 去添加关于请求授权规则的代码。关于shiro内置过滤器的权限设置有:

  • anon:无需认证就可以访问
  • authc:必须认证了才能访问
  • user:必须拥有记住我功能才能使用
  • perms:拥有对某个资源的权限才能访问
  • role:拥有某个角色权限才能访问

ShiroConfig:

package com.al.shirospringboot.config;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
    //创建realm对象,自定义
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }

    //DefaultWebSecurityManager
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // securityManager 安全管理器关联 Realm (userRealm)
        securityManager.setRealm(userRealm);
        return securityManager;
    }
    //ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);
        // 添加shiro内置的过滤器
        Map<String, String> filterMap = new LinkedHashMap<>();
        //filterMap.put("/user/*", "authc"); //支持通配符
        // 授权。正常情况下,没有授权的会跳转到未授权界面
        filterMap.put("/user/add", "perms[user:add]");
        filterMap.put("/user/update", "authc");
        bean.setFilterChainDefinitionMap(filterMap);

        //设置登录的请求
        bean.setLoginUrl("/login");
        return bean;
    }
}

测试:访问登录页面localhost:8080/login , 输入正确的用户名和密码点击提交登录进入到登录页面。点击add,会出现401 错误。

filterMap.put(“/user/add”, “perms[user:add]”); 这个表示 必须是 user用户,使用add方法,才能授权进行访问。这时测试会出现401错误,是因为没有授权,且我们没有设置未授权页面。所以只是在这里显示401。不然会跳转至未授权的页面。

编写未授权界面

我们编写未授权界面。在访问没有权限的页面时,跳转到未授权页面。

为了未授权 不显示401错误,我们在MyController类中添加了一个未授权页面:

    @RequestMapping("/noauth")
    @ResponseBody
    public String unauthorized() {
        return "未经授权,无法访问此页面";
    }

然后同样在shiroFilterFactoryBean方法中设置未授权页面的请求.ShiroConfig类中:

    //ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ......
        // 未授权页面
        bean.setUnauthorizedUrl("/noauth");
        return bean;
    }

启动测试一下,登录用户,认证用户登录成功,可以成功访问 update 页面。但是无法访问 add 页面(因为没有权限),则跳转到未授权页面。

image-20220507112329775

给用户授予权限

我们在前面已经添加了授权规则,shiro 中的SecurityManager会在登录拦截用户,认证用户可以成功登录。对于没有权限的用户,则无法执行权限之外的操作。

用户认证和权限信息都放在 realm 对象中,在前面,我们已经读取数据库中用户信息,存放在realm对象中。与用户认证类似,我们也在 Realm对象 UserRealm类中去添加用户权限信息,给用户授权。

UserRealm:

  • 这里是给了所有的用户都赋予user:add的权限。
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    System.out.println("执行了=>授权doGetAuthorizationInfo");
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    info.addStringPermission("user:add");
    return info;
}

我们重启访问,成功登录后可以访问/add页面。

但是对用户的授权不应该放在此,应该设置在数据库中。我们给user表新增一个字段perms,用于表示用户的权限信息。

image-20220507123321077

给不同的用户添加不同的权限:

image-20220507123926196

同步实体类:

package com.al.shirospringboot.pojo;

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

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;
    private String name;
    private String pwd;
    private String perms;
}

然后我们需要在授权方法中拿到当前用户的资源,这时候只需要将认证方法中的principal参数传入,即可取出:

package com.al.shirospringboot.config;

import com.al.shirospringboot.pojo.User;
import com.al.shirospringboot.service.UserServiceImpl;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;

// 自定义Realm
public class UserRealm extends AuthorizingRealm {
    @Autowired
    UserServiceImpl userService;

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("执行了=>授权doGetAuthorizationInfo");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //info.addStringPermission("user:add");
        // 获取当前的 Subject
        Subject subject = SecurityUtils.getSubject();
        //通过subject获取当前user
        User CurrentUser = (User) subject.getPrincipal();
        //设置当前user的权限(从数据库中读取)
        info.addStringPermission(CurrentUser.getPerms());
        return info;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了=>认证doGetAuthenticationInfo");
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        /**
        //伪造正确用户名和密码
        String username = "alzn";
        String password = "123456";
        //用户名认证
        if (!userToken.getUsername().equals(username))
            return null;//只需要return null,就会自动抛出UnknownAccountException异常
        //密码认证,涉及到安全问题,shiro自动完成
        return new SimpleAuthenticationInfo("", password, "zsr");
         */
        //连接真实的数据库
        User user = userService.queryUserByName(userToken.getUsername());
        //用户名认证
        if (user == null)
            return null;//只需要return null,就会自动抛出UnknownAccountException一场
        //密码认证,涉及到安全问题,shiro自动完成
        return new SimpleAuthenticationInfo("", user.getPwd(), "");
    }
}

此时的ShiroConfig 代码为:

package com.al.shirospringboot.config;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
    //创建realm对象,自定义
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }

    //DefaultWebSecurityManager
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // securityManager 安全管理器关联 Realm (userRealm)
        securityManager.setRealm(userRealm);
        return securityManager;
    }
    //ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);
        // 添加shiro内置的过滤器
            /*
            anon:无需认证就可以访问
            authc:必须认证了才能访问
            user:必须拥有记住我功能才能使用
            perms:拥有对某个资源的权限才能访问
            role:拥有某个角色权限才能访问
         */
        Map<String, String> filterMap = new LinkedHashMap<>();
        //filterMap.put("/user/add","anon");
        //filterMap.put("/user/update","authc");
        //filterMap.put("/user/*", "authc"); //支持通配符
        // 授权。正常情况下,没有授权的会跳转到未授权界面
        filterMap.put("/user/add", "perms[user:add]");
        filterMap.put("/user/update", "perms[user:update]");
        bean.setFilterChainDefinitionMap(filterMap);

        //设置登录的请求
        bean.setLoginUrl("/login");
        // 未授权页面
        bean.setUnauthorizedUrl("/noauth");
        return bean;
    }
}

重启测试一下,

  • 我们登录alzn用户,可以成功进入add页面,无法进入update页面

  • 如果登录al用户,可以成功进入update页面,无法进入add页面

  • 如果登录zn用户,则两个页面都没有权限

Shiro整合Thymeleaf

如果我们想要实现在首页登录后,只显示拥有权限用户的对应页面信息。如:

  • user:add 权限用户只显示 add 超链接的页面
  • user:update

这时候使用 thymeleaf 可以完成这项任务。

导入依赖

shiro-thymeleaf整合:

<!-- https://mvnrepository.com/artifact/com.github.theborakompanioni/thymeleaf-extras-shiro -->
<dependency>
  <groupId>com.github.theborakompanioni</groupId>
  <artifactId>thymeleaf-extras-shiro</artifactId>
  <version>2.0.0</version>
</dependency>

编写配置

在shiro配置类ShiroConfig中编写对应的配置:

//ShiroDialect: 用来shiro整合thymeleaf
@Bean
public ShiroDialect getShiroDialect() {
    return new ShiroDialect();
}

修改index.html

我们要实现在首页,拥有对应权限的用户只显示对应的超链接,然后添加一个登录按钮

首先导入shiro的命名空间:

xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"

index.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>首页</h1>
    <p>
        <a th:href="@{/login}">登录</a>
    </p>
<p th:text="${msg}"></p>
<hr>
<!--<a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">update</a>-->
<div shiro:hasPermission="user:add">
    <a th:href="@{/user/add}">add</a>
</div>
<div shiro:hasPermission="user:update">
    <a th:href="@{/user/update}">update</a>
</div>
</body>
</html>

重启测试,访问http://localhost:8080/

image-20220507154339744

但是测试的时候,输入用户和密码登录时 出现错误:

java.lang.ClassCastException: java.lang.String cannot be cast to com.al.shirospringboot.pojo.User
	at com.al.shirospringboot.config.UserRealm.doGetAuthorizationInfo(UserRealm.java:29) ~[classes/:na]

image-20220507145754373

获取的信息是字符串 String,无法转换成 User 类型。无法进行类型转换。

查找错误原因:

//通过subject获取当前user
User CurrentUser = (User) subject.getPrincipal();
//Object o = subject.getPrincipal();

这里获得是用户名,不是一个User具体对象。

调试:

// 获取当前的 Subject
Subject subject = SecurityUtils.getSubject();
//通过subject获取当前user
//User CurrentUser = (User) subject.getPrincipal();
Object o = subject.getPrincipal();
System.out.println(o);

结果输出的是:执行了=>授权doGetAuthorizationInfo。

【搜索之后,发现,这里使用 SecurityUtils.getSubject().getPrincipal() 获取当前的user。找到 当前user有在用户认证的时候出现,存在了 Relam中。】

错误的来源是由于在Realm自定义对象 UserRelam 中的认证部分的代码出错了。这个subject.getPrincipal(); 中的principal是由用户认证传递的。在我们的原代码中:

返回结果是:

return new SimpleAuthenticationInfo("" , user.getPwd(), "");

关于这个类 SimpleAuthenticationInfo:

public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) {
    this.principals = new SimplePrincipalCollection(principal, realmName);
    this.credentials = credentials;
}

可以发现:

  • SimpleAuthenticationInfo 在用户认证时会传递 principal。principal作为身份的标识,既可以传用户名,也可以传实体类的对象,

我一开始就是因为传的principal是空 “”,所以后面SecurityUtils.getSubject().getPrincipal() 没有获得User实体类信息,当我改成对象 user后,就没问题了。

UserRealm 类的整体代码:

package com.al.shirospringboot.config;

import com.al.shirospringboot.pojo.User;
import com.al.shirospringboot.service.UserServiceImpl;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;

// 自定义Realm
public class UserRealm extends AuthorizingRealm {
    @Autowired
    UserServiceImpl userService;

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("执行了=>授权doGetAuthorizationInfo");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //info.addStringPermission("user:add");
        // 获取当前的 Subject
        Subject subject = SecurityUtils.getSubject();
        //通过subject获取当前user
        User CurrentUser = (User) subject.getPrincipal();
        //Object o = subject.getPrincipal();
        //System.out.println(o);
        //设置当前user的权限(从数据库中读取)
        info.addStringPermission(CurrentUser.getPerms());
        return info;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了=>认证doGetAuthenticationInfo");
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        /**
        //伪造正确用户名和密码
        String username = "alzn";
        String password = "123456";
        //用户名认证
        if (!userToken.getUsername().equals(username))
            return null;//只需要return null,就会自动抛出UnknownAccountException异常
        //密码认证,涉及到安全问题,shiro自动完成
        return new SimpleAuthenticationInfo("", password, "zsr");
         */
        //连接真实的数据库
        User user = userService.queryUserByName(userToken.getUsername());
        //用户名认证
        if (user == null)
            return null;//只需要return null,就会自动抛出UnknownAccountException一场

        //将用户信息存入session
        Subject subject = SecurityUtils.getSubject();
        Session session = subject.getSession();
        session.setAttribute("loginUser",user);

        //密码认证,涉及到安全问题,shiro自动完成
        //return new SimpleAuthenticationInfo("" , user.getPwd(), "");
        return new SimpleAuthenticationInfo(user, user.getPwd(),"");
    }
}

重新测试: http://localhost:8080/

在登录用户名和密码为 alzn 123456时,可以成功登录,且成功进入 add.html页面:

image-20220507160318767

image-20220507160418780

关于 zn 20082022用户时:什么都无法显示。

image-20220507160504429

一切都是我们想要的了。ok!

不显示登录按钮

如果我们想登录成功后,就不让其显示 登录按钮了。 我们需要增加一个 登录判断:

  • 我们判断的时候,要根据 session里面是否存在东西进行判断, 所以我们还要在 MyController 类中进行传递给前端 session这个信息。

不过这里把这个 session的数据放在了 UserRealm类中:

// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    System.out.println("执行了=>认证doGetAuthenticationInfo");
    UsernamePasswordToken userToken = (UsernamePasswordToken) token;
    /**
    //伪造正确用户名和密码
    String username = "alzn";
    String password = "123456";
    //用户名认证
    if (!userToken.getUsername().equals(username))
        return null;//只需要return null,就会自动抛出UnknownAccountException异常
    //密码认证,涉及到安全问题,shiro自动完成
    return new SimpleAuthenticationInfo("", password, "zsr");
     */
    //连接真实的数据库
    User user = userService.queryUserByName(userToken.getUsername());
    //用户名认证
    if (user == null)
        return null;//只需要return null,就会自动抛出UnknownAccountException一场

    //将用户信息存入session
    Subject subject = SecurityUtils.getSubject();
    Session session = subject.getSession();
    session.setAttribute("loginUser",user);

    //密码认证,涉及到安全问题,shiro自动完成
    return new SimpleAuthenticationInfo("", user.getPwd(), "");
}

首页 index.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>首页</h1>
<p th:if="${session.loginUser}==null">
    <a th:href="@{/login}">登录</a>
</p>
<p th:text="${msg}"></p>
<hr>
<!--<a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">update</a>-->
<div shiro:hasPermission="user:add">
    <a th:href="@{/user/add}">add</a>
</div>
<div shiro:hasPermission="user:update">
    <a th:href="@{/user/update}">update</a>
</div>
</body>
</html>
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐