• Java实现手机验证码登录和SpringSecurity权限控制


    手机验证码登录和SpringSecurity权限控制

    手机快速登录功能,就是通过短信验证码的方式进行登录。这种方式相对于用户名密码登录方式,用户不需要记忆自己的密码,只需要通过输入手机号并获取验证码就可以完成登录,是目前比较流行的登录方式。

    由于身份受限,只能基于模拟的方式进行验证码发送,而不能通过手机短信实现(开通需要认证资质或上线项目)

    1、前端页面根据手机号请求发送验证码

    使用了axios框架作为异步请求的方式,直接将后端生产的代码通过对话框的形式显示,模拟手机验证

    //发送验证码
    sendValidateCode(){
        var telephone = this.loginInfo.telephone;
        if (!checkTelephone(telephone)) {
            this.$message.error('请输入正确的手机号');
            return false;
        }
        validateCodeButton = $("#validateCodeButton")[0];
        clock = window.setInterval(doLoop, 1000); //一秒执行一次
        axios.post("/validateCode/send4Login.do?telephone=" + telephone).then((response) => {
            if(!res.data.flag){
                this.$notify.error({
                    title: '错误',
                    message: res.data.message
                });
            }else {
                this.$notify({
                    title: '成功',
                    message: res.data.data + res.data.message,
                    type: 'success'
                });
            }
        });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    封装checkTelephone校验手机号

    /**
     * 手机号校验
     1--以1为开头;
     2--第二位可为3,4,5,7,8,中的任意一位;
     3--最后以0-9的9个整数结尾。
     */
    function checkTelephone(telephone) {
        var reg=/^[1][3,4,5,7,8][0-9]{9}$/;
        if (!reg.test(telephone)) {
            return false;
        } else {
            return true;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    封装doLoop每秒调用一起的刷新验证码时间

    var clock = '';//定时器对象,用于页面30秒倒计时效果
    var nums = 30;
    var validateCodeButton;
    //基于定时器实现30秒倒计时效果
    function doLoop() {
        validateCodeButton.disabled = true;//将按钮置为不可点击
        nums--;
        if (nums > 0) {
            validateCodeButton.value = nums + '秒后重新获取';
        } else {
            clearInterval(clock); //清除js定时器
            validateCodeButton.disabled = false;
            validateCodeButton.value = '重新获取验证码';
            nums = 30; //重置时间
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2、后端生产验证码并保存到Redis中

    验证码保存在Redis服务器中,需要远程调用获取到对象进行操作,这里是Spring项目所以Redsi的连接信息等都交由Spring创建完成了

    RedisMessageConstant.SENDTYPE_LOGIN = 002区别代号

    /**
     * 注入redis连接对象
     */
    @Autowired
    private JedisPool jedisPool;
    
    /**
     * 手机验证码登录
     * @param telephone
     * @return
     */
    @RequestMapping("/send4Login")
    public Result send4Login(String telephone){
        // 1、随机生成4位数的验证码
        try {
            Integer code = ValidateCodeUtils.generateValidateCode(6);
            // 2、将验证码保存到redis,存活时间5分钟秒,同时调用不同场景的后缀进行区分
            jedisPool.getResource().setex(
                    telephone + RedisMessageConstant.SENDTYPE_LOGIN,5 * 60,code.toString());
            // 3、将验证码返回给页面提示
            return new Result(true, MessageConstant.SEND_VALIDATECODE_SUCCESS+",请在五分钟内进行操作!",code);
        } catch (Exception e) {
            e.printStackTrace();
            // 2、将验证码返回给页面提示
            return new Result(false, MessageConstant.SEND_VALIDATECODE_FAIL);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    前端点击登录事件跳过不写

    3、手机登录后端代码编写

    3.1、Controller登录控制器编写

    在health_mobile工程中创建MemberController并提供login方法进行登录检查,处理逻辑为:

    1、校验用户输入的短信验证码是否正确,如果验证码错误则登录失败

    2、如果验证码正确,则判断当前用户是否为会员,如果不是会员则自动完成会员注册

    3、向客户端写入Cookie,内容为用户手机号

    4、将会员信息保存到Redis,使用手机号作为key,保存时长为30分钟

    package com.zcl.controller;
    
    import com.alibaba.dubbo.config.annotation.Reference;
    import com.alibaba.fastjson.JSON;
    import com.itheima.constant.MessageConstant;
    import com.itheima.constant.RedisMessageConstant;
    import com.itheima.entity.Result;
    import com.itheima.pojo.Member;
    import com.zcl.service.MemberService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import redis.clients.jedis.JedisPool;
    
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletResponse;
    import java.util.Date;
    import java.util.Map;
    
    /**
     * 项目名称:health_parent
     * 描述:处理会员相关的控制器
     *
     * @author zhong
     * @date 2022-06-23 15:36
     */
    @RestController
    @RequestMapping("/member")
    public class MemberController {
    
        /**
         * 获取到保存的验证码
         */
        @Autowired
        private JedisPool jedisPool;
    
        /**
         * 远程调用会员服务器
         */
        @Reference
        private MemberService memberService;
    
        /**
         * 手机快速登录
         * @param map 接收验证码和手机号
         * @return
         */
        @RequestMapping("/login")
        public Result login(HttpServletResponse response, @RequestBody Map map){
            // 获取登录手机号
            String telephone = (String) map.get("telephone");
            // 获取redis的验证码
            String validCode = jedisPool.getResource().get(telephone + RedisMessageConstant.SENDTYPE_LOGIN);
            // 获取页面提交的验证码
            String validateCode = (String) map.get("validateCode");
            // 判断三项信息是否为空
            if(telephone != null && validCode != null && validateCode != null && validCode.equals(validateCode)){
                // 验证码正确
                Member member = memberService.findByTelephone(telephone);
                if(member == null){
                    member = new Member();
                    member.setRegTime(new Date());
                    member.setPhoneNumber(telephone);
                    // 不是会员,自动完成会员注册
                    memberService.add(member);
                }
                // 写客户端写入Cookie,内容为手机号
                //写入Cookie,跟踪用户
                Cookie cookie = new Cookie("login_member_telephone",telephone);
                cookie.setPath("/");
                cookie.setMaxAge(60*60*24*30);
                response.addCookie(cookie);
                //保存会员信息到Redis中
                // Redis是不能直接保存java对象的的,需要转换对象
                String json = JSON.toJSON(member).toString();
                jedisPool.getResource().setex(telephone,60*30,json);
                return new Result(true,MessageConstant.LOGIN_SUCCESS);
            }else{
                return new Result(false, MessageConstant.VALIDATECODE_ERROR);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    业务层接口和实现类下面省略不写,都是简单的【根据手机号查询、创建会员】等信息

    页面登录够可以查看浏览器的Cookie是否有保存到用户的手机号信息

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yj2mLSFl-1656040522354)(images/image-20220623164941082.png)]

    查看Redis可视化工具查看保存的用户登录信息

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8qmcMpXd-1656040522355)(images/image-20220623165014120.png)]

    成功登录之后会跳转到会员页面

    4、权限控制

    4.1、认证和权限授权概念

    认证:系统提供的用于识别用户身份的功能,通常提供用户名和密码进行登录其实就是在进行认证,认证的目的是让系统知道你是谁。

    授权:用户认证成功后,需要为用户授权,其实就是指定当前用户可以操作哪些功能。

    本章节就是要对后台系统进行权限控制,其本质就是对用户进行认证和授权。

    4.2、权限模块数据模型

    分析了认证和授权的概念,要实现最终的权限控制,需要有一套表结构支撑:

    用户表t_user、权限表t_permission、角色表t_role、菜单表t_menu、用户角色关系表t_user_role、角色权限关系表t_role_permission、角色菜单关系表t_role_menu。

    表之间关系如下图:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Z8apQ8U-1656040522356)(images/image-20220623170722669.png)]

    通过上图可以看到,权限模块共涉及到7张表。在这7张表中,角色表起到了至关重要的作用,其处于核心位置,因为用户、权限、菜单都和角色是多对多关系。

    接下来我们可以分析一下在认证和授权过程中分别会使用到哪些表:

    认证过程:只需要用户表就可以了,在用户登录时可以查询用户表t_user进行校验,判断用户输入的用户名和密码是否正确。

    授权过程:用户必须完成认证之后才可以进行授权,首先可以根据用户查询其角色,再根据角色查询对应的菜单,这样就确定了用户能够看到哪些菜单。然后再根据用户的角色查询对应的权限,这样就确定了用户拥有哪些权限。所以授权过程会用到上面7张表。

    关于实体类的创建

    上面一共有7张标,而实体类创建只需要创建4个,表和表的多对多关系通过在属性字段上使用List<属性>来表示,如下:

    private Set<Role> roles = new HashSet<Role>(0);//角色集合
    private List<Menu> children = new ArrayList<>();//子菜单集合
    
    • 1
    • 2

    4.3、Spring Security简介

    Spring Security是 Spring提供的安全认证服务的框架。 使用Spring Security可以帮助我们来简化认证和授权的过程。官网:https://spring.io/projects/spring-security

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9po75F7w-1656040522356)(images/image-20220623172308749.png)]

    4.4、核心依赖坐标

    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-web</artifactId>
      <version>5.0.5.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-config</artifactId>
      <version>5.0.5.RELEASE</version>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    常用的权限框架除了Spring Security,还有Apache的shiro框架。

    5、Spring Security入门案例

    5.1、创建SpringSecurityDemoMaven演示工程

    打包方式为war,web项目

    提供index.html页面,内容为hello Spring Security!!

    5.2、依赖管理pom.xml
    <!--权限核心依赖-->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>5.0.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
        <version>5.0.5.RELEASE</version>
    </dependency>
    <!-- Spring -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jms</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    5.3、配置web.xml文件

    在web.xml中主要配置SpringMVC的DispatcherServlet和用于整合第三方框架的DelegatingFilterProxy,用于整合Spring Security。

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
             version="4.0">
        <filter>
            <!--
              DelegatingFilterProxy用于整合第三方框架
              整合Spring Security时过滤器的名称必须为springSecurityFilterChain,
              否则会抛出NoSuchBeanDefinitionException异常
            -->
            <filter-name>springSecurityFilterChain</filter-name>
            <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        </filter>
        <filter-mapping>
            <filter-name>springSecurityFilterChain</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
        <servlet>
            <servlet-name>springmvc</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <!-- 指定加载的配置文件 ,通过参数contextConfigLocation加载 -->
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>classpath:spring-security.xml</param-value>
            </init-param>
            <load-on-startup>1</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>springmvc</servlet-name>
            <url-pattern>*.do</url-pattern>
        </servlet-mapping>
        </servlet-mapping>
    </web-app>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    5.4、配置spring-security.xml文件

    核心命名空间:

    xmlns:security="http://www.springframework.org/schema/security"
    <!--约束-->
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security.xsd">
    
    • 1
    • 2
    • 3
    • 4

    开启表达式use-expressions="true",下面的就需要使用access="hasRole('ROLE_ADMIN')",如果为false使用access="ROLE_ADMIN"

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
           xmlns:mvc="http://www.springframework.org/schema/mvc"
           xmlns:security="http://www.springframework.org/schema/security"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
    						http://www.springframework.org/schema/beans/spring-beans.xsd
    						http://www.springframework.org/schema/mvc
    						http://www.springframework.org/schema/mvc/spring-mvc.xsd
    						http://code.alibabatech.com/schema/dubbo
    						http://code.alibabatech.com/schema/dubbo/dubbo.xsd
    						http://www.springframework.org/schema/context
    						http://www.springframework.org/schema/context/spring-context.xsd
                              http://www.springframework.org/schema/security
                              http://www.springframework.org/schema/security/spring-security.xsd">
    
        <!--
            http:用于定义相关权限控制
            auto-config:是否自动配置
                            设置为true时框架会提供默认的一些配置,例如提供默认的登录页面、登出处理等
                            设置为false时需要显示提供登录表单配置,否则会报错
            use-expressions:用于指定intercept-url中的access属性是否使用表达式
        -->
        <security:http auto-config="true" use-expressions="true">
            <!--
                intercept-url:定义一个拦截规则
                pattern:对哪些url进行权限控制
                access:在请求对应的URL时需要什么权限,默认配置时它应该是一个以逗号分隔的角色列表,
    				  请求的用户只需拥有其中的一个角色就能成功访问对应的URL
            -->
            <security:intercept-url pattern="/**"  access="hasRole('ROLE_ADMIN')" />
        </security:http>
    
        <!--
            authentication-manager:认证管理器,用于处理认证操作
        -->
        <security:authentication-manager>
            <!--
                authentication-provider:认证提供者,执行具体的认证逻辑
            -->
            <security:authentication-provider>
                <!--
                    user-service:用于获取用户信息,提供给authentication-provider进行认证
                -->
                <security:user-service>
                    <!--
                        user:定义用户信息,可以指定用户名、密码、角色,后期可以改为从数据库查询用户信息
    				  {noop}:表示当前使用的密码为明文
                    -->
                    <security:user name="admin" password="{noop}admin" authorities="ROLE_ADMIN"></security:user>
                </security:user-service>
            </security:authentication-provider>
        </security:authentication-manager>
    </beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    5.5、快速入门测试

    启动项目,浏览器访问Tomcat指定的端口

    该入门案例使用的是5.0.5.RELEASE版本的依赖包,如果使用是新版本,那么配置可能就会报错,因为有些旧的技术会被抛弃

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-28HF9HRw-1656040522357)(images/image-20220623202830103.png)]

    原因分析:

    访问端口默认是访问index.html页面的,由于我们在web.xml配置了拦截进入到Security框架,如下:

    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    
    • 1
    • 2
    • 3
    • 4

    spring-security.xml配置文件中配置了权限拦截器,必须要拥有这个角色才可以访问,如下

    <security:http auto-config="true" use-expressions="true">
        <!--
            intercept-url:定义一个拦截规则
            pattern:对哪些url进行权限控制
            access:在请求对应的URL时需要什么权限,默认配置时它应该是一个以逗号分隔的角色列表,
                  请求的用户只需拥有其中的一个角色就能成功访问对应的URL
        -->
        <security:intercept-url pattern="/**"  access="hasRole('ROLE_ADMIN')" />
    </security:http>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    得益于auto-config="true",判断没有该角色就自动的跳转到security的默认登录页面

    又因为在配置文件中指定了账号和密码配置,在登录页面输入就可以访问index.html页面了

    <security:user name="admin" password="{noop}1234" authorities="ROLE_ADMIN"></security:user>
    
    • 1

    在后面的改进中会逐步的完成根据数据库信息来完成登录

    输出账号密码后会先跳转访问favicon.ico,因为没有该图标就会报404错误,直接访问index.html就可以访问不会被拦截了

    6、对入门案例进行改进

    通过入门案例我们可以看到,Spring Security将我们项目中的所有资源都保护了起来,要访问这些资源必须要完成认证而且需要具有ROLE_ADMIN角色,这就与真是的生产环境差太远了,不如登录页面页被拦截了这是不可的

    6.1、配置可匿名访问的资源

    创建pages文件夹,在里面创建a.html和b.html页面 ,在没有修改代码之前进行路径访问是不可以访问到的,会被拦截到登录页面,当配置如下的时候页面就可以直接访问了而不需要通过登录的方法鉴权

    <!--配置哪些资源匿名可以访问的资源(不登录页可以访问)-->
    <!--<security:http security="none" pattern="/pages/a.html"></security:http>
    <security:http security="none" pattern="/pages/b.html"></security:http>-->
    <security:http security="none" pattern="/pages/**"></security:http>
    
    • 1
    • 2
    • 3
    • 4
    6.2、指定使用的登录页面
    1. 通过login.html登录页面

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>Title</title>
      </head>
      <body>
      <h1>自定义登录页面</h1>
      <form action="/login.do" method="post">
          username:<input type="text" name="username"><br>
          password:<input type="password" name="password"><br>
          <input type="submit" value="submit">
      </form>
      </body>
      </html>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
    2. 修改spring-security.xml配置文件,指定long.html页面可以匿名访问

      <security:http security="none" pattern="/login.html"></security:http>
      
      • 1
    3. 修改spring-security.xml文件,加入表单登录信息的配置

      1、login-page:指定登录页面

      2、username-parameter:登录名

      3、password-parameter:登录密码

      4、login-processing-url:登录请求地址,由框架完成

      5、default-target-url:登录成功后的跳转页面

      6、authentication-failure-url:登录失败的跳转页面

      <security:http auto-config="true" use-expressions="true">
              <!--如果我们要使用自己指定的页面作为登录页面,必须配置登录表单-->
              <security:form-login
                  login-page="/login.html"
                  username-parameter="username"
                  password-parameter="password"
                  login-processing-url="/login.do"
                  default-target-url="/index.html"
                  authentication-failure-url="/login.html"/>
      </security:http>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    4. 修改spring-security.xml文件,关闭CsrfFilter过滤器

      该代码放在<security:form-login/>标签下面

      <!--
        csrf:对应CsrfFilter过滤器
        disabled:是否启用CsrfFilter过滤器,如果使用自定义登录页面需要关闭此项,否则登录操作会被禁用(403)
      -->
      <security:csrf disabled="true"></security:csrf>
      
      • 1
      • 2
      • 3
      • 4
      • 5

      如果是默认的false,框架在提交表单的时候会生成一个如下的东西来判断是否安全的

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s027wJBw-1656040522358)(images/image-20220623222012725.png)]

      在提交数据的时候会将这个串与内存中的进行对比是否一样,如果是一样的那么就是框架自己的登录页面是安全的,如果不相同的就会认为是不安全的(伪造的)登录数据,框架就会禁止后面的操作(报403)

    6.3、模拟从数据库查询用户信息

    如果我们要从数据库动态查询用户信息,就必须按照spring security框架的要求提供一个实现UserDetailsService接口的实现类,并按照框架的要求进行配置即可。框架会自动调用实现类中的方法并自动进行密码校验。

    创建接口实现类【后面完善,现在只是测试】
    package com.zcl.service;
    
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    
    /**
     * 项目名称:SpringSecurityDemo
     * 描述:springSecurit框架查询数据库实现类
     *
     * @author zhong
     * @date 2022-06-23 22:27
     */
    public class SpringSecurityUserService implements UserDetailsService {
        /**
         * 根据用户名查询用户信息【不需要自己调用,由框架调用】
         * @param username 登录用户名
         * @return
         * @throws UsernameNotFoundException
         */
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            System.out.println("用户输入的登录名为:"+username);
            return null;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    修改spring-security.xml配置文件

    创建实现类的bean对象交给spring,同时将写死的登录数据改为引用实现类bean的对象

    <!--authentication-manager:认证管理器,用于处理认证操作-->
    <security:authentication-manager>
        <!--authentication-provider:认证提供者,执行具体的认证逻辑-->
        <security:authentication-provider user-service-ref="userService"/>
    </security:authentication-manager>
    
    <!--将实现类注册到spring容器中-->
    <bean id="userService" class="com.zcl.service.SpringSecurityUserService"/>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    启动项目,拦截到登录页面输入账号密码点击登录,查看IDEA控制台调用SpringSecurityUserService类的方法,可以看到已经输出里面的打印语句了,下面就会在该类中完善登录查询数据库的方法

    模拟完善SpringSecurityUserService类实现数据库查询用户信息和角色授予
    package com.zcl.service;
    
    import com.zcl.pojo.User;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    /**
     * 项目名称:SpringSecurityDemo
     * 描述:springSecurit框架查询数据库实现类
     *
     * @author zhong
     * @date 2022-06-23 22:27
     */
    public class SpringSecurityUserService implements UserDetailsService {
        /**
         * 模拟数据库
         */
        public static Map<String, User> map = new HashMap<>();
        static {
            // 创建两个用户对象
            User user1 = new User();
            user1.setUsername("admin");
            user1.setPassword("1234");
    
            User user2 = new User();
            user2.setUsername("lisi");
            user2.setPassword("1234");
    
            // 添加用户到map集合中
            map.put(user1.getUsername(), user1);
            map.put(user2.getUsername(), user2);
        }
    
        /**
         * 根据用户名查询用户信息【不需要自己调用,由框架调用】
         * @param username 登录用户名
         * @return
         * @throws UsernameNotFoundException
         */
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 根据用户名查询数据库获取到用户信息(包含数据库存储的密码信息)
            User user = map.get(username);
            // 判断查询结果
            if(user == null){
                return null;
            }else{
                // 将用用户信息返回给框架
                List<GrantedAuthority> list = new ArrayList<>();
                // 模拟权限(后期需要改为从数据库查询当前用户所具有的权限)
                list.add(new SimpleGrantedAuthority("permission_A"));
                list.add(new SimpleGrantedAuthority("permission_B"));
                // 模拟根据角色授权
                if ("admin".equals(username)) {
                    // 授予角色
                    list.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
    
                }
                // 框架回对密码进行对比(页面和查询的密码对比)
                org.springframework.security.core.userdetails.User securityUser = new org.springframework.security.core.userdetails.User(username,"{noop}"+user.getPassword(),list);
                return securityUser;
            }
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    在上面的设置的设置的判断是否指定用户名有角色信息,如果没有的话点击登录哪怕是登录进去了也不可以访问指定的资源信息,因为我们在配置文件中指定了角色权限,访问效果如下

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PRReZd94-1656040522358)(images/image-20220624064850516.png)]

    在上面定义用户密码的时候时候,如果不使用{noop}指定明文,登录的时候也会报一个密码为空的错位信息,错误提示如下

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IlyysH2h-1656040522359)(images/image-20220624065044291.png)]

    6.4、对密码进行加密处理

    在上面的案例中都是使用了文明密码,是很不安全的,一般情况下都需要对密码进行加密处理,常见的加密方式有如下几种:

    1. 3DES、AES、DES:使用对称加密算法,可以通过解密来还原出原始密码
    2. MD5、SHA1:使用单向HASH算法,无法通过计算还原出原始密码,但是可以建立彩虹表进行查表破解
    3. bcrypt:将salt随机并混入最终加密后的密码,验证时也无需单独提供之前的salt,从而无需单独处理salt问题

    加密后的格式一般为:

    $2a$10$/bTVvqqlH9UiE0ZJZ7N2Me3RIgUCdgMheyTgV0B4cMCSokPa.6oCa
    
    • 1

    加密后字符串的长度为固定的60位。其中:$是分割符,无意义;2a是bcrypt加密版本号;10是cost的值;而后的前22位是salt值;再然后的字符串就是密码的密文了。

    步骤一:在spring-security.xml文件中指定密码加密对象
    <!--配置密码加密对象-->
    <bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
    
    • 1
    • 2

    注入到安全框架

    <security:authentication-manager>
        <!--authentication-provider:认证提供者,执行具体的认证逻辑-->
        <security:authentication-provider user-service-ref="userService">
            <!--引用密码加密处理bean-->
            <security:password-encoder ref="passwordEncoder"/>
        </security:authentication-provider>
    </security:authentication-manager>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    步骤二:修改UserService实现类

    将原来静态代码块方法改为没有返回值的方法,这样框架在调用的时候就可以调用方法直接初始化完成密码的加密,使用了@Autowired注解注入了在配置文件中创建的加密bean,同时需要在配置文件中开启注解,否则是@Autowired是无效的也会报错

    /**
     * 注入加密密码的对象
     */
    @Autowired
    private BCryptPasswordEncoder PasswordEncoder;
    
    /**
     * 模拟数据库
     */
    public  Map<String, User> map = new HashMap<>();
    public void initUserData() {
        // 创建两个用户对象
        User user1 = new User();
        user1.setUsername("admin");
        // 使用bcrypt进行加密处理
        user1.setPassword(PasswordEncoder.encode("1234"));
    
        User user2 = new User();
        user2.setUsername("lisi");
        user2.setPassword(PasswordEncoder.encode("1234"));
    
        // 添加用户到map集合中
        map.put(user1.getUsername(), user1);
        map.put(user2.getUsername(), user2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    前置密码加密后,在调用loadUserByUsername接口实现类的,需要将原来通过{noop}指定暗文密码的就可以不需要了,因为上面已近对密码进行加密

    // 框架回对密码进行对比(页面和查询的密码对比)
    org.springframework.security.core.userdetails.User securityUser = new org.springframework.security.core.userdetails.User(username,user.getPassword(),list);
    
    • 1
    • 2

    6.5、配置多种校验规则

    首先在项目中创建a.html、b.html、c.html、d.html几个页面

    <!--只要认证通过就可以访问-->
    <security:intercept-url pattern="/index.jsp"  access="isAuthenticated()" />
    <security:intercept-url pattern="/a.html"  access="isAuthenticated()" />
    
    <!--拥有add权限就可以访问b.html页面-->
    <security:intercept-url pattern="/b.html"  access="hasAuthority('add')" />
    
    <!--拥有ROLE_ADMIN角色就可以访问c.html页面-->
    <security:intercept-url pattern="/c.html"  access="hasRole('ROLE_ADMIN')" />
    
    <!--拥有ROLE_ADMIN角色就可以访问d.html页面,
    	注意:此处虽然写的是ADMIN角色,框架会自动加上前缀ROLE_-->
    <security:intercept-url pattern="/d.html"  access="hasRole('ADMIN')" />
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在上面的配置中,对于isAuthenticated()不起效,当用户b登录后不能访问,a用户有权限的可以访问,如果有知道的小伙伴可以留言我

    6.6、注解方式权限认证

    Spring Security除了可以在配置文件中配置权限校验规则,还可以使用注解方式控制类中方法的调用。例如Controller中的某个方法要求必须具有某个权限才可以访问,此时就可以使用Spring Security框架提供的注解方式进行控制。

    1. 在spring-security.xml文件中配置组件扫描,用于扫描Controller
      <mvc:annotation-driven></mvc:annotation-driven>
      <context:component-scan base-package="com.zcl.controller"></context:component-scan>
      
      • 1
      • 2

      创建controller

    2. 在spring-security.xml文件中开启权限注解支持
      <!--开启注解方式权限控制-->
      <security:global-method-security pre-post-annotations="enabled" />
      
      • 1
      • 2

      如果不开启使用注解是不行的

    3. 创建Controller类并在Controller的方法上加入注解进行权限控制
      package com.zcl.controller;
      
      import org.springframework.security.access.prepost.PreAuthorize;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      /**
       * 项目名称:SpringSecurityDemo
       * 描述:权限注解类使用
       *
       * @author zhong
       * @date 2022-06-24 10:43
       */
      @RestController
      @RequestMapping("/hello")
      public class HelloController {
      
          /**
           * 必须具有add权限才可以访问方法
           * @return
           */
          @RequestMapping("/add")
          @PreAuthorize("hasAuthority('add')")
          public String add(){
              System.out.println("add......");
              return "Hello Spring Security";
          }
      
          /**
           * 必须具有ROLE_ADMIN角色才可以被访问
           * @return
           */
          @RequestMapping("/delete")
          @PreAuthorize("hasRole('ROLE_ADMIN')")
          public String delete(){
              System.out.println("delete......");
              return "Hello Spring Security";
          }
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40

      启动项目测试

      1. 通过第一个登录信息lisi登录是访问不了上面两个方法的,因为指定了add需要具有add权限
      2. 访问delete需要具有角色ROLE_ADMIN才能进行访问
      3. 登录admin账号就可以访问了

    7、退出登录

    用户完成登录后Spring Security框架会记录当前用户认证状态为已认证状态,即表示用户登录成功了。那用户如何退出登录呢?我们可以在spring-security.xml文件中进行如下配置:

    放在</security:http>标签里面

    <!--
      logout:退出登录
      logout-url:退出登录操作对应的请求路径
      logout-success-url:退出登录后的跳转页面
    -->
    <security:logout logout-url="/logout.do" 
                     logout-success-url="/login.html" invalidate-session="true"/>
           
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    通过上面的配置可以发现,如果用户要退出登录,只需要请求/logout.do这个URL地址就可以,同时会将当前session失效,最后页面会跳转到login.html页面。

  • 相关阅读:
    C#自定义控件:提示未将对象引用设置到对象实例
    【MindSpore易点通机器人-02】设计与技术选型
    【vue3|第20期】vue3中Vue Router路由器工作模式
    机器学习-集成学习(模型融合)方法概述
    基于开源模型对文本和音频进行情感分析
    【论文笔记】Deep Multi-View Spatial-Temporal Network for Taxi Demand Prediction
    CAMX模型大气臭氧来源解析模拟与臭氧成因分析实践技术应用
    Android recycleview瀑布流中间穿插一行占满一屏
    Python 3.11新功能:错误信息回溯
    brew对redis的使用
  • 原文地址:https://blog.csdn.net/baidu_39378193/article/details/125442014