Sringboot2整合shiro实现登录认证、权限控制和记住我功能

7 分钟

前言

很久没写过文章了,最近课程设计硬性要求要使用Spring Secure或shiro其中一种安全框架,因为shiro的轻便更被广泛使用,所以学了一下shiro框架的基本使用,也来了点兴致,所以记录一下。


提示:以下是本篇文章正文内容,下面案例可供参考

一、关于Shiro

  1. Apache Shiro 是一个功能强大且易于使用的Java安全框架,为开发人员提供了一种直观而全面的身份验证,授权,加密和会话管理解决方案,提供了身份验证、授权、密码学、会话管理四大基本安全功能、还提供一些缓存、记住我、并发等多种额外功能解决一些问题。
  2. Shiro的一些核心概念有:Realms、SecutiryManager、Subjuect。
  3. 关于Subject可直接理解为对象,用于和应用进行交互,通过Subject传入到安全管理器。
  4. 关于SecurityManger就是安全管理器,所有的安全性操作都与它进行交互,是Shiro管理的核心。
  5. 关于Realms可以理解为存储域,存储Shiro的一些安全数据等,安全管理器需要从Realms获取数据,验证是否合法。
大概就这些,学的比较粗浅,底层源码也没咋了解,直接就学了如何使用。

二、功能实现

1.创建User表的数据库

  1. 实现Shiro与数据库交互,能够通过手机号+密码或邮箱+密码进行登录验证。
    在这里插入图片描述
这里的数据库字段设置如上、Id号(主键)、name(姓名)、phone(手机号)、username(用户名)、password(密码)、email(邮箱)、state(状态)。

2.引入依赖

pom.xml文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>shiroDemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shiroDemo</name>
    <description>shiroDemo</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.17</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.10.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.10.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

3.配置application.yaml文件

当然也可以配置application.properties,个人习惯使用yaml,这里配置mybatis和连接mysql数据库,如下:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/teamwork?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
mybatis:
  #config-location: classpath:mybatis/mybatis-config.xml  #全局配置文件位置
  mapper-locations: classpath:mybatis/mapper/*.xml  #sql映射文件位置
  configuration:
    map-underscore-to-camel-case: true

4.完善Mybatis

  1. 创建User类如下:
package com.example.shirodemo.Bean;

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

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private Integer id;
    private String name;
    private String phone;
    private String username;
    private String password;
    private String email;
    private Integer state;
}
  1. 创建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.example.shirodemo.mapper.UserMapper">
</mapper>
package com.example.shirodemo.mapper;

import com.example.shirodemo.Bean.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface UserMapper {
    @Select("select *from user where password=#{password} limit 1")
    User ShiroAuthority(String password);
    @Select("select password from user where phone=#{phone} ")
    User Login(String phone);
    @Select("select password from user where email=#{email}")
    User LoginByEmail(String email);
}
这里用两个Select语句,当前端传入数据第一种方式查询不到user用户时,就进行第二种,以此来实现两种登录方式,当然也可以一个方法直接写入到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.example.homework1.mapper.UserMapper">
    <select id="login" resultType="com.example.homework1.bean.User">
        select *from User
    <where>
            <if test="phone!=null and phone!=''">
               and phone=#{phone}
            </if>
            <if test="email!=null and email!=''">
               and email=#{email}
            </if>
            and password= #{password}
    </where>
    </select>

5.重写Realm和Shiro配置类

  1. 因为数据库中存储的密码是用的base64+md5加密存储,所以这里要先写一个MD5的加密类,如下:
package com.example.shirodemo.config;


import org.springframework.util.DigestUtils;

import java.util.Base64;

import static com.mysql.cj.util.StringUtils.getBytes;

public class MD5 {
    /**
     * @param text明文
     * @param key密钥
     * @return 密文
     * 对数据库内的密码存储进行md5的加密
     */
    public static String md5(String password,String key) throws Exception {
            String base64encodedString = Base64.getEncoder().encodeToString((password + key).getBytes("utf-8"));
            // 加密后的字符串
            return DigestUtils.md5DigestAsHex(getBytes(base64encodedString));


        }

}
  1. 重写Realm方法
package com.example.shirodemo.config;

import com.example.shirodemo.Bean.User;
import com.example.shirodemo.mapper.UserMapper;
import com.example.shirodemo.service.UserService;
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.springframework.beans.factory.annotation.Autowired;

import java.util.Collections;


public class MyRealm extends AuthorizingRealm {
    @Autowired
    UserService userService;
    @Autowired
    UserMapper userMapper;
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        User user = (User) principals.getPrimaryPrincipal();
        if (user != null) {
            User users = (User) userMapper.ShiroAuthority(user.getPassword());
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();//权限信息
            if (users.getUsername().equals("admin")) {
                System.out.println(users.getName());
                simpleAuthorizationInfo.addRole("admin");
            } else {
                simpleAuthorizationInfo.addRole("user");
            }
            return simpleAuthorizationInfo;
        }
        return  null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {
        String username= (String) authcToken.getPrincipal();
        // 获取用户密码
        User user;
        user = userService.Login(username);
        if (user == null) {
            // 没找到帐号
            user =userService.LoginByEmail(username);
            if(user==null)
                throw new UnknownAccountException();
        }
        // 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配
        return new SimpleAuthenticationInfo(user, user.getPassword(),getName());
    }


}
这里主要重写两个方法,第一个方法用于进行权限验证,这里用不到,下面一个方法用于进行登录验证,主要从Token中取得用户名,然后是否存在此用户,不存在则报错,存在则交给CredentialsMatcher进行密码进行密码匹配,principals用于控制角色,通过Autoken中的对象查询数据库的用户名,判断是否是admin,添加角色用于判断。
  1. ShiroConfiguration类
package com.example.shirodemo.config;

import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

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

@Configuration
public class ShiroConfiguration {
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * Shiro生命周期处理器
     * @return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 自动创建代理
     * @return
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
    /**
     解决MyRealm中UserMapper一直是null的问题
     **/
    @Bean
    MyRealm myRealm() {
        return new MyRealm();
    }
    /**
     *安全管理器
     * **/
    @Bean
    SecurityManager securityManager() {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(myRealm());
        manager.setRememberMeManager(rememberMeManager());
        manager.setSessionManager(sessionManager());
        return manager;
    }

    @Bean
    ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //指定 SecurityManager
        bean.setSecurityManager(securityManager());
        //登录页面
        bean.setLoginUrl("/login");
        //登录成功页面
        bean.setSuccessUrl("/");
        //访问未获授权路径时跳转的页面
        bean.setUnauthorizedUrl("/login");
        //配置路径拦截规则,注意,要有序
        Map<String, String> map = new LinkedHashMap<>();
        map.put("/doLogin", "anon");
        map.put("/logout","logout");
        map.put("/**", "user");
        bean.setFilterChainDefinitionMap(map);
        return bean;
    }
    /**
     但是再shiro:1.6开始,新增了一个InvalidRequestFilter的过滤器,用于拦截存在安全问题的uri,不进行配置在首次访问/时,URL中会出现jsessionid,并返回400错误
     * **/
    @Bean
    public DefaultWebSessionManager sessionManager(){
        DefaultWebSessionManager sessionManager=new DefaultWebSessionManager();
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return  sessionManager;
    }
    /**设置Cookie**/
    @Bean
    public SimpleCookie rememberMeCookie() {
        SimpleCookie cookie = new SimpleCookie("rememberMe");
        cookie.setMaxAge(86400);
        return cookie;
    }

    /**
     * 设置cookie管理对象和Cookie的加密密钥,默认是AES加密
     * @return
     */
    @Bean
    public CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
        return cookieRememberMeManager;
    }

}

将logout写入filterChainDefinitionMap链中就可以自动实现注销功能,注意UserMapper类和跳转被拦截的问题即可,CookieRememberMeManager和 SimpleCookie类主要用于记住我功能后生成返回Cookie,前面三个类使@RequireRole()注解的角色控制生效。

在这里插入图片描述

6.前端和Controller层

  1. Controller层——IndexController
package com.example.shirodemo.Controller;

import com.example.shirodemo.Bean.User;
import com.example.shirodemo.config.MD5;
import com.example.shirodemo.mapper.UserMapper;
import com.example.shirodemo.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;


@Controller
public class IndexController {
    @GetMapping("/login")
    public String login(){
        return "login";
    }

    @GetMapping("/")
    public String index(Model model) {
        model.addAttribute("message", "登录成功");
        return "index";
    }

    @PostMapping("/doLogin")
    public String doLogin(String username, String password, boolean rememberMe,Model model){
        Subject subject= SecurityUtils.getSubject();
        try {
            String surepassword= MD5.md5(password,"Aiwin");
            UsernamePasswordToken token=new UsernamePasswordToken(username, surepassword);
            token.setRememberMe(rememberMe);
            subject.login(token);
            model.addAttribute("message","登录成功");
            return "index";
        } catch (AuthenticationException e) {
            e.printStackTrace();
            model.addAttribute("message","登录失败");
            return "login";
        } catch (Exception e) {
            e.printStackTrace();
            return  "login";
        }
    }

}
主要是Suject类,将接受到的用户和正确的密码提交到Token,通过与Subject类进行交互进行login,MyRealm中取得的用户名和密码可以理解为就是从这里Token交上去的。

HomeController类:

package com.example.shirodemo.Controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {
    @GetMapping("/home")
    public  String home(){
        return "hello";
    }
    @RequiresRoles("admin")
    @GetMapping("/third")
    public String third(){
            return "third";
    }
}
  1. 前端页面
    Index.html:
<!DOCTYPE html>
<html lang="en"  xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/html">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<label style="color: red" th:text="${message}"></label>
<a th:href="@{/logout}">注销</a>
</body>
</html>

login.html

<!DOCTYPE html>
<html lang="en"  xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/html">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<label style="color: red" th:text="${message}"></label>
<a th:href="@{/logout}">注销</a>
<a th:href="@{/home}">跳转</a>
<a th:href="@{/third}">跳转管理员页面</a>
</body>
</html>

hello.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>第二个页面</title>
</head>
<body>
<h1>这是第二个页面</h1>
</body>
</html>

third.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>第三个页面</title>
</head>
<body>
<h1>只能管理员访问哦</h1>
</body>
</html>
至此,功能全部实现,实现效果图如下

在这里插入图片描述
在这里插入图片描述

直接关闭浏览器,然后再访问/,可以直接跳转到登录成功的页面,点击注销再访问则会跳转到登录页面,应该是通过Cookie中的rememberMe中取得user的信息进行验证判断是否通过。
~  ~  The   End  ~  ~


 赏 
承蒙厚爱,倍感珍贵,我会继续努力哒!
logo图像
tips
(*) 4 + 6 =
快来做第一个评论的人吧~