SpringBoot 整合 Shiro Thymeleaf Mysql 动态授权

需求安排

需求分析:
1. 匿名允许访问页面首页和登录页面
2. 默认登录页面
3. admin用户只有用户添加权限
4. test用户只有用户更新权限
5. 访问授权页面,默认跳转登录页面
6. 登录认证通过,访问无权限页面,默认跳转未授权页面
7. 登录认证通过,进行资源授权角色授权后,访问各自有权限的页面
注:第6条企业内部采用无权限的页面采用shiro和页面整合策略,直接不会显示

一、前期准备

1. maven依赖

 <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <druid.version>1.0.28</druid.version>
        <mybatis-spring-boot.version>2.1.2</mybatis-spring-boot.version>
        <shiro-spring.version>1.5.2</shiro-spring.version>
        <thymeleaf-extras-shiro.version>2.0.0</thymeleaf-extras-shiro.version>
    </properties>

    <dependencies>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--thymeleaf模板引擎-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!--springMvc启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- shiro与spring整合依赖 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro-spring.version}</version>
        </dependency>
        <!--mybatis启动器-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis-spring-boot.version}</version>
        </dependency>
        <!--MYSQL-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- 集成druid数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <!-- thymel对shiro的扩展坐标 -->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>${thymeleaf-extras-shiro.version}</version>
        </dependency>
    </dependencies>

2. 创建数据库+初始化表数据

数据库名:shiro
表名:user

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名',
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
  `perms` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '授权字段',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'admin', 'admin', 'user:add');
INSERT INTO `user` VALUES (2, 'test', 'test', 'user:update');

3. 实体类

package com.gblfy.entity;

import lombok.Data;
import java.io.Serializable;

@Data
public class User implements Serializable {
    private Integer id;
    private String name;
    private String password;
    private String perms;
}

4. mapper接口

package com.gblfy.mapper;

import com.gblfy.entity.User;

public interface UserMapper {

    /**
     * 通过id查询用户信息
     *
     * @param id
     * @return
     */
    User selectByPrimaryKey(Integer id);

    /**
     * 根据用户名查询用户信息
     *
     * @param name
     * @return
     */
    User selectByName(String name);

}

5. mapper接口映射文件

在resources目录下面创建mapping文件夹

<?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.gblfy.mapper.UserMapper">
    <resultMap id="BaseResultMap" type="com.gblfy.entity.User">
        <id column="id" property="id" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <result column="password" property="password" jdbcType="VARCHAR"/>
        <result column="perms" property="perms" jdbcType="VARCHAR"/>
    </resultMap>

    <sql id="Base_Column_List">
    id, name, password, perms
  </sql>

    <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer">
        select
        <include refid="Base_Column_List"/>
        from user
        where id = #{id,jdbcType=INTEGER}
    </select>

    <select id="selectByName" resultMap="BaseResultMap" parameterType="java.lang.String">
        select
        <include refid="Base_Column_List"/>
        from user
        where name = #{name,jdbcType=VARCHAR}
    </select>
</mapper>

6. service接口

package com.gblfy.service;

import com.gblfy.entity.User;

public interface UserService {
    /**
     * 根据用户名查询用户信息
     *
     * @param name
     * @return
     */
    User selectByName(String name);
    /**
     * 通过id查询用户信息
     *
     * @param id
     * @return
     */
    User selectByPrimaryKey(Integer id);
}

7. service实现类

package com.gblfy.service.impl;

import com.gblfy.entity.User;
import com.gblfy.mapper.UserMapper;
import com.gblfy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 根据用户名查询用户信息
     *
     * @param name
     * @return
     */
    @Override
    public User selectByName(String name) {
        return userMapper.selectByName(name);
    }

    /**
     * 通过id查询用户信息
     *
     * @param id
     * @return
     */
    @Override
    public User selectByPrimaryKey(Integer id) {
        return userMapper.selectByPrimaryKey(id);
    }

}

8. 首页控制器

package com.gblfy.controller;

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

@Controller
public class IndexController {

    @RequestMapping("index")
    public String index() {
        return "index";
    }
}

9. 用户控制器

package com.gblfy.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
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.RequestMapping;

@Controller
public class UserController {

    //跳转登录页面
    @RequestMapping("/toLogin")
    public String toLogin() {
        return "login";
    }


    //用户添加
    @RequestMapping("/add")
    public String add() {
        return "/user/add";
    }

    //用户更新
    @RequestMapping("/update")
    public String update() {
        return "/user/update";
    }

    //未授权跳转页面
    @RequestMapping("noAuth")
    public String noAuth() {
        return "noAuth";
    }

    //登录处理
    @RequestMapping("login")
    public String login(String username, String password, Model model) {
        /**
         * 编写shiro认证操作
         * 1.获取subject
         * 2.封装用户数据
         * 3.执行登录方法
         */
        //1.获取subject
        Subject subject = SecurityUtils.getSubject();

        //2.封装用户数据 认证时会用到
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);

        //3.执行登录方法
        try {
            subject.login(token);
            //登陆成功
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            //登陆失败 场景1
            model.addAttribute("msg", "用户名不存在!");
            return "login";

        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            //登陆失败 场景2
            model.addAttribute("msg", "密码错误!");
            return "login";

        }
        //登陆成功
        model.addAttribute("msg", username + "登陆成功跳转首页");
        return "index";
    }
}

10. 全局配置文件

#server 端口
server:
  port: 80
  tomcat:
    min-spare-threads: 30
    max-threads: 1000

#Mysql配置
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro?useSSL=true&serverTimezone=GMT&allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource

# Mybatis Mapper映射文件+实体类配置
mybatis:
  type-aliases-package: com.gblfy.entity
  mapperLocations:
    - classpath*:/mapping/*.xml


11. 启动类加注解

package com.gblfy;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.gblfy.mapper")
public class SpringbootShiroApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootShiroApplication.class, args);
    }

}

二、页面准备

2.1. 首页页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

<head>
    <meta charset="UTF-8">
    <title>系统首页</title>
</head>
<body>
<h3 th:text="${msg}" style="color: red"></h3>
<div>
    进入用户添加页面:&ensp;&ensp;&ensp;<a href="/add">用户添加</a>
</div>
<br/>
<div>
    进入用户更新页面:&ensp;&ensp;&ensp;<a href="/update">用户更新</a>
</div>
</body>
</html>

2.2. 登录页面

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
<h3 th:text="${msg}" style="color: red">登录</h3>
<form method="post" action="/login">
    用户名:<input type="text" name="username"/><br/>&ensp;码:<input type="password" name="password"/><br/>
    <input type="submit" value="登录"/>
</form>
</body>
</html>

2.3. 未授权页面

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <title>未授权页面</title>
</head>
<body>
未授权页面,请联系管理员进行授权操作!
</body>
</html>

2.4. 用户添加页面

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <title>添加页面</title>
</head>
<body>
<h3>添加页面</h3>
</body>
</html>

2.5. 用户更新页面

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <title>更新页面</title>
</head>
<body>
<h3>更新页面</h3>
</body>
</html>

三、Shiro 配置

3.1. 自定义UserRealm

package com.gblfy.realm;

import com.gblfy.entity.User;
import com.gblfy.service.UserService;
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;

/**
 * 自定义 UserRealm
 * 固定写法:extends AuthorizingRealm
 */
public class UserRealm extends AuthorizingRealm {


    @Autowired
    private UserService userService;

    /**
     * 认证
     *
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1.得到用户名和密码    因为登录已存,因此,能取
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        String username = usernamePasswordToken.getUsername();

        //默认获取password的类型为char[],转换处理
        String password = new String(usernamePasswordToken.getPassword());

        //2.从数据库根据用户名查询用户信息
        User user = userService.selectByName(username);
        //判断查询出的用户对象(sysUser)是否为空
        if (user == null) {
            throw new UnknownAccountException("用户不存在!");
        }
        //判断查询出的用户对象的用户密码和页面从页面传递过来的密码进行比较是否相同
        if (!user.getPassword().equals(password)) {
            throw new IncorrectCredentialsException("密码有误!");
        }

        //认证通过 登陆成功
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
        return info;
    }

    /**
     * 授权
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        //通过id用户信息
        Subject subject = SecurityUtils.getSubject();
        //认证时存的是user,因此可以强转成user对象
        User user = (User) subject.getPrincipal();
        User userPerms = userService.selectByPrimaryKey(user.getId());

        //从user对象动态获取权限,放到认证容器
        info.addStringPermission(userPerms.getPerms());
        return info;
    }


}

3.2. ShiroConfig

package com.gblfy.config;

import com.gblfy.realm.UserRealm;
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 {

    /**
     * 1.创建Realm
     */
    @Bean(name = "userRealm")
    public UserRealm getRealm() {
        return new UserRealm();
    }

    /**
     * 2.创建DefaultWebSecurityManager
     */
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //关联realm
        securityManager.setRealm(userRealm);
        return securityManager;
    }

    /**
     * 3.创建ShiroFilterFactoryBean
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //指定登录页面
        shiroFilterFactoryBean.setLoginUrl("/toLogin");
        //指定未授权页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth");

        //添加Shiro内置过滤器
        /**
         * Shiro内置过滤器,可以实现权限相关的拦截器
         *    常用的过滤器:
         *       anon: 无需认证(登录)可以访问
         *       authc: 必须认证才可以访问
         *       user: 如果使用rememberMe的功能可以直接访问
         *       perms: 该资源必须得到资源权限才可以访问
         *       role: 该资源必须得到角色权限才可以访问
         */
        Map<String, String> filterMap = new LinkedHashMap<String, String>();

        //设置允许匿名访问
        filterMap.put("/index", "anon");
        filterMap.put("/login", "anon");

        //配置授权过滤器 第一种
        filterMap.put("/add", "perms[user:add]");
        filterMap.put("/update", "perms[user:update]");

        //授权才可以访问
        filterMap.put("/*", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);

        return shiroFilterFactoryBean;
    }

}

四、需求测试

测试计划;
1. 访问http://localhost/index和http://localhost/login 是否可以匿名访问
2. 访问http://localhost/index页面,点击进入添加页面是否跳转登录页面
3. 访问http://localhost/index页面,点击进入更新页面是否跳转登录页面
4. 访问几个不存在的url测试是否默认统一跳转登录页面。例如: http://localhost/xxx
5. 使用admin账户,访问登录页面,做一下测试:
 1>输入不存在的账户点击登录,不输密码,测试,是否提示用户名不存在
 2>输入存在的账户admin不输密码,点击登录,测试,是否提示密码错误
 3>输入存在的正确的账户admin,输入正确的密码,点击登录,测试,点击用户添加,是否可以进入用户添加页面
 4>输入存在的正确的账户admin,输入正确的密码,点击登录,测试,点击用户更新,是否跳转未授权页面
6. 使用test账户,访问登录页面,做一下测试:
 1>输入不存在的账户点击登录,不输密码,测试,是否提示用户名不存在
 2>输入存在的账户test不输密码,点击登录,测试,是否提示密码错误
 3>输入存在的正确的账户test,输入正确的密码,点击登录,测试,点击用户更新,是否可以进入用户更新页面
 4>输入存在的正确的账户test,输入正确的密码,点击登录,测试,点击用户添加,是否跳转未授权页面

到此,除了成功登录后,角色授权,就都演示完了!

五、源码链接

发布了979 篇原创文章 · 获赞 153 · 访问量 26万+

猜你喜欢

转载自blog.csdn.net/weixin_40816738/article/details/105472064