话不多说,直接上代码。
认真观看代码,里面含有大量的注释
github上的代码,修改一下数据库的信息,就可以直接使用。附带sql文件
遗憾的是,没有实现RBAC
github地址:https://github.com/ZiCheng-Web/springboot-shiro
0、创建springboot项目(springboot-shiro)
项目结构
1、修改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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zicheng</groupId>
<artifactId>springboot-shiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springboot-shiro</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<!-- 修改thymeleaf的版本 -->
<!--<thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>-->
<!--<thymeleaf-layout-dialect.version>2.0.4</thymeleaf-layout-dialect.version>-->
</properties>
<dependencies>
<!--springboot Start-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--springboot End-->
<!-- mybatis Start -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<!-- mybatis End -->
<!--shiro start-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.5.1</version>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<!--Shiro End-->
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--lombok-->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.zicheng.SpringBootShiroApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
2、修改配置文件
server:
port: 8080
spring:
thymeleaf:
cache: false
prefix: classpath:/templates/
suffix: .html
encoding: UTF-8
mode: HTML5
datasource:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3306/testshiro?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
type-aliases-package: com.zicheng.domain
mapper-locations: classpath:mapper/*.xml
3、shiro的配置文件
package com.zicheng.shiro;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
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;
/**
* 子诚
* Description:Shiro的配置类
* 时间:2020/3/7 14:20
*/
@Configuration
public class ShiroConfig {
/**
* 创建ShiroFilterFactoryBean(shiro过滤器工厂实例)
*/
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager securityManager) {
//创建ShiroFilterFactoryBean的实例
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//关联->安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//修改跳转的登陆页面
shiroFilterFactoryBean.setLoginUrl("/toLogin");
//设置未授权提示页面
shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth");
//添加shiro的内置过滤器
/**
* shiro的内置过滤器,可以实现权限相关的拦截
* 常用的过滤器:
* 1、anon :无需认证(无需登录),即可访问
* 2、authc :必须认证才可以访问(可以理解为登陆了才能访问)
* 3、user :如果使用rememberMe(登陆框下面的记住我功能)的功能就可以直接访问
* 4、perms :该资源必须得到资源权限才能好访问
* 5、role: 该资源必须得到角色权限才能访问
*/
Map<String, String> filterMap = new LinkedHashMap<String, String>();
//将登陆请求,放行
filterMap.put("/login", "anon");
//anon:无需认证即可访问(要在下面的所有到需要认证的上面,顺序要注意)
//filterMap.put("/test", "anon");
// filterMap.put("/add", "authc");
// filterMap.put("/update", "authc");
//授权过滤器
//注意:当前授权拦截后,shiro会自动跳转到未授权页面
filterMap.put("/add", "perms[user:add]");
filterMap.put("/update", "perms[user:update]");
//所有资源链接都必须登陆认证才能访问,一定要写在最后面
filterMap.put("/*", "authc");
//setFilterChainDefinitionMap:设置过滤器定义Map
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
/**
* 创建DefaultWebSecurityManager
* <p>
* getDefaultWebSecurityManager()
*/
@Bean(name = "defaultWebSecurityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
//@Qualifier("userRealm"):代表着从spring的bean工厂中userRealm为合格者
//创建 DefaultWebSecurityManager的实例 securityManager
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联UserRealm
securityManager.setRealm(userRealm);
return securityManager;
}
/**
* 创建Realm
*/
@Bean(name = "userRealm")
public UserRealm getRealm() {
return new UserRealm();
}
/**
* 配置ShiroDialect,用于thymeleaf和shiro标签配合使用
*/
@Bean
public ShiroDialect getShiroDialect() {
return new ShiroDialect();
}
}
package com.zicheng.shiro;
import com.zicheng.domain.User;
import com.zicheng.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;
/**
* 子诚
* Description:自定义的Realm,继承shiro提供的AuthorizingRealm
* 时间:2020/3/7 14:24
*/
public class UserRealm extends AuthorizingRealm {
/**
* 执行授权逻辑
* AuthorizationInfo 用于聚合授权信息;
* 当我们使用 AuthorizingRealm 时,
* 如果身份验证成功,在进行授权时就通过doGetAuthorizationInfo 方法获取角色/权限信息用于授权验证。
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了授权逻辑");
//给资源进行授权
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//添加资源的授权字符串
//info.addStringPermission("user:add");
//到数据库查询当前登录用户的授权字符串
//获取当前登录用户
Subject subject = SecurityUtils.getSubject();
User user = (User)subject.getPrincipal();
User dbUser = userSerivce.findById(user.getId());
info.addStringPermission(dbUser.getPerms());
return info;
}
@Autowired
private UserService userSerivce;
/**
* 执行认证逻辑
* AuthenticationInfo:用于进行认证用户名和密码的。以及密码是否加盐,是否加密
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了认证逻辑");
//编写shiro判断逻辑,判断用户名和密码
//1、判断用户名
UsernamePasswordToken token= (UsernamePasswordToken) authenticationToken;
//根据页面上填写的用户名,获取到整个user
User user = userSerivce.findByName(token.getUsername());
if(null == user){
//意味着用户名不存在
return null;//shiro的底册会抛出一个UnknownAccountException:这个异常代表用户名不存在
}
//2、判断密码
return new SimpleAuthenticationInfo(user,user.getPassword(),getName());
/**
* SimpleAuthenticationInfo中可以传三个参数也可以传四个参数。
* 第一个参数:
* 传入的都是com.zicheng.domain 包下的User类的user对象。
* 第二个参数:
* 传入的是从数据库中获取到的password,然后再与token中的password进行对比,匹配上了就通过,匹配不上就报异常。
* 第三个参数:
* 盐–用于加密密码对比。 若不需要,则可以设置为空 “ ”
* 第四个参数:
* 当前realm的名字。
*/
}
}
4、DAO层
package com.zicheng.mapper;
import com.zicheng.domain.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.web.bind.annotation.Mapping;
@Mapper
public interface UserMapper {
public User findByName(String username);
public User findById(Integer id);
}
<?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">
<!-- 该文件存放CRUD的sql语句 -->
<mapper namespace="com.zicheng.mapper.UserMapper">
<select id="findByName" parameterType="string" resultType="com.zicheng.domain.User">
SELECT id,
username,
PASSWORD
FROM
t_user where username = #{value}
</select>
<select id="findById" parameterType="int" resultType="com.zicheng.domain.User">
SELECT id,
username,
PASSWORD,
perms
FROM
t_user where id = #{value}
</select>
</mapper>
5、service层
package com.zicheng.service;
import com.zicheng.domain.User;
public interface UserService {
public User findByName(String username);
public User findById(Integer id);
}
package com.zicheng.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.zicheng.domain.User;
import com.zicheng.mapper.UserMapper;
import com.zicheng.service.UserService;
@Service
public class UserServiceImpl implements UserService{
//注入Mapper接口
@Autowired
private UserMapper userMapper;
@Override
public User findByName(String username) {
return userMapper.findByName(username);
}
@Override
public User findById(Integer id) {
return userMapper.findById(id);
}
}
6、Controller层
package com.zicheng.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
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;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpSession;
import java.lang.annotation.IncompleteAnnotationException;
/**
* 子诚
* Description:
* 时间:2020/3/6 7:02
*/
@Controller
public class UserController {
/**
* 跳转到增加用户页面
*/
@RequestMapping("/add")
public String add() {
return "/user/add";
}
/**
* 跳转到更新用户页面
*/
@RequestMapping("/update")
public String update() {
return "/user/update";
}
/**
* 跳转到未授权错误页面
*/
@RequestMapping("/noAuth")
public String noAuth(){
return "/noAuth";
}
/**
* 跳转到登陆页面
*/
@RequestMapping("/toLogin")
public String toLogin() {
return "login";
}
/**
* 登陆逻辑处理
* UsernamePasswordToken是一个简单的包含username及password即用户名及密码的登录验证用token,
* 这个类同时继承了HostAuthenticationToken及RememberMeAuthenticationToken,
* 因此UsernamePasswordToken能够存储客户端的主机地址以及确定token是否能够被后续的方法使用。
* 值得注意的是在这里password是以char数组的形式存储的,这样设计是因为String本身是不可变的,
* 这可能会导致无效的密码被访问到。
* 另外,开发者应在验证之后调用clear方法清除token从而杜绝该token在后续被访问的可能性。
*/
@RequestMapping("/login")
public String login(String username, String password, Model model, HttpSession session) {
/**
* 使用shiro编写认证操作
*/
//1、获取subject
Subject subject = SecurityUtils.getSubject();
//2、获取用户数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//3、执行登陆方法
try {
subject.login(token);
//没有异常就是登陆成功,转发到主页面
session.setAttribute("username",username);
return "redirect:/test";
} catch (UnknownAccountException e) {
//任何异常,都代表着登陆失败
// UnknownAccountException:这个异常代表用户名不存在
model.addAttribute("msg", "用户名不存在");
return "login";
} catch (IncorrectCredentialsException e) {
//IncorrectCredentialsException:代表着密码不正确
model.addAttribute("msg", "密码错误");
return "login";
}
}
/**
* 测试方法
*/
@RequestMapping("/hello")
@ResponseBody
public String hello() {
System.out.println("UserController.hello()");
return "ok";
}
/**
* 测试thymeleaf
*/
@RequestMapping("/test")
public String testThymeleaf(Model model) {
//返回test.html
return "test";
}
}
7、实体类
package com.zicheng.domain;
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 password;
private String perms;
}
8、前端页面
test.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8">
<title>测试Thymeleaf的使用</title>
</head>
<body>
欢迎您!<b><span th:text="${session.username}"></span></b>
<hr/>
<div shiro:hasPermission="user:add">
进入用户添加功能: <a href="add">用户添加</a><br/>
</div>
<div shiro:hasPermission="user:update">
进入用户更新功能: <a href="update">用户更新</a><br/>
</div>
<a href="/toLogin">登录</a>
</body>
</html>
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>登陆</title>
<style>
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background: #34495e;
}
.box {
width: 300px;
padding: 40px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #191919;
text-align: center;
}
.box h1 {
color: white;
text-transform: uppercase;
font-weight: 500;
}
.box input[type="text"],
.box input[type="password"] {
border: 0;
background: none;
display: block;
margin: 20px auto;
text-align: center;
border: 2px solid #3498db;
padding: 14px 10px;
width: 200px;
outline: none;
color: white;
border-radius: 24px;
transition: 0.25s;
}
.box input[type="text"]:focus,
.box input[type="password"]:focus {
width: 280px;
border-color: #2ecc71;
}
.box input[type="submit"] {
border: 0;
background: none;
display: block;
margin: 20px auto;
text-align: center;
border: 2px solid #2ecc71;
padding: 14px 40px;
outline: none;
color: white;
border-radius: 24px;
transition: 0.25s;
}
.box input[type="submit"]:hover {
background: #2ecc71;
}
</style>
</head>
<body>
<form class="box" action="/login" method="post">
<h1>Login</h1>
<h4 th:text="${msg}" style="color: red"></h4>
<input type="text" name="username" placeholder="Username">
<input type="password" name="password" placeholder="Password">
<input type="submit" value="Login">
</form>
</body>
</html>
noAth.html(错误信息的页面)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>未授权提示页面</title>
</head>
<body>
<div align="center">
<h1>未经授权,无法访问该页面:</h1>
<h3><a href="/test">返回主页面</a></h3>
<h3><a href="/toLogin">返回登录页面</a></h3>
</div>
</body>
</html>
add.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>用户添加页面</title>
</head>
<body>
<h1>用户添加页面</h1>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>用户更新页面</title>
</head>
<body>
<h1>用户更新页面</h1>
</body>
</html>