因为工作原因,项目中使用了Springboot 整合Shiro来做权限的整合,项目接近尾声,我也按照网上的资料自己搭了一个,踩了一些坑。下面是步骤
目录
1.1 首先是搭建一个springboot 的web项目 这个就不多说了
2.6 修改我们的HomeController中的/login请求
(一) 基础工程
图片是项目搭建结构图
1.1 首先是搭建一个springboot 的web项目 这个就不多说了
1.2 依赖引入
<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.bobby.springboot</groupId>
<artifactId>boot_shrio</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>boot_shrio Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--thymeleaf 模板-->
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.22</version>
</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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--shiro 依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!-- mp 依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>2.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
</project>
1.3 这里我们用到thymeleaf模版
application.yml:在这个地方我遇到了一个问题 就是我不指定templates目录,使用spriboot默认的static文件夹下,但访问不了。
已解决访问static中index文件夹下的index.html页面
spring:
thymeleaf:
suffix: .html
encoding: UTF-8
content-type: text/html
mode: HTML5
prefix: classpath:/templates/
application:
name: shiro
datasource:
url: jdbc:mysql://127.0.0.1:3306/bobby?useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
jpa:
database: mysql
showSql: true
hibernate:
ddlAuto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5Dialect
format_sql: true
1.4 创建页面
index.html、userAdd.html、userDel.html、userInfo.html中只需要显示一句简单的文字即可。
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
错误信息:<h4 style="color: red;" th:text="${msg}"></h4>
<form th:action="@{/login}" method="post">
<p>账号:<input type="text" name="username" value="admin"/></p>
<p>密码:<input type="password" name="password" value="123456"/></p>
<p><input type="submit" value="登录"/></p>
</form>
</body>
</html>
1.5 在entity包下把实体类创建起来。
SysPermission
package com.bobby.springboot.entity;
/**
* Created by Bobby on 2019/9/19 17:23
*/
import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
@Entity
@Table(name = "sys_permission")
public class SysPermission implements Serializable {
private static final long serialVersionUID = 1L;
@Id@GeneratedValue
private long id;//主键.
private String name;//名称.
@Column(columnDefinition="enum('menu','button')")
private String resourceType;//资源类型,[menu|button]
private String url;//资源路径.
private String permission; //权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view
private Long parentId; //父编号
private String parentIds; //父编号列表
private Boolean available = Boolean.FALSE;
@ManyToMany
@JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="permissionId")},inverseJoinColumns={@JoinColumn(name="roleId")})
private List<SysRole> roles;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getResourceType() {
return resourceType;
}
public void setResourceType(String resourceType) {
this.resourceType = resourceType;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getPermission() {
return permission;
}
public void setPermission(String permission) {
this.permission = permission;
}
public Long getParentId() {
return parentId;
}
public void setParentId(Long parentId) {
this.parentId = parentId;
}
public String getParentIds() {
return parentIds;
}
public void setParentIds(String parentIds) {
this.parentIds = parentIds;
}
public Boolean getAvailable() {
return available;
}
public void setAvailable(Boolean available) {
this.available = available;
}
public List<SysRole> getRoles() {
return roles;
}
public void setRoles(List<SysRole> roles) {
this.roles = roles;
}
@Override
public String toString() {
return "SysPermission [id=" + id + ", name=" + name + ", resourceType=" + resourceType + ", url=" + url
+ ", permission=" + permission + ", parentId=" + parentId + ", parentIds=" + parentIds + ", available="
+ available + ", roles=" + roles + "]";
}
}
userinfo
package com.bobby.springboot.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.List;
/**
* Created by Bobby on 2019/9/23 11:13
*/
@Entity
@Table(name = "user_info")
public class UserInfo implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue
private long uid;//用户id;
@Column(unique=true)
private String username;//账号.
private String name;//名称(昵称或者真实姓名,不同系统不同定义)
private String password; //密码;
private String salt;//加密密码的盐
private byte state;//用户状态,0:创建未认证(比如没有激活,没有输入验证码等等)--等待验证的用户 , 1:正常状态,2:用户被锁定.
@ManyToMany(fetch= FetchType.EAGER)//立即从数据库中进行加载数据;
@JoinTable(name = "SysUserRole", joinColumns = { @JoinColumn(name = "uid") }, inverseJoinColumns ={@JoinColumn(name = "roleId") })
private List<SysRole> roleList;// 一个用户具有多个角色
public List<SysRole> getRoleList() {
return roleList;
}
public void setRoleList(List<SysRole> roleList) {
this.roleList = roleList;
}
public long getUid() {
return uid;
}
public void setUid(long uid) {
this.uid = uid;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
public byte getState() {
return state;
}
public void setState(byte state) {
this.state = state;
}
/**
* 密码盐.
* @return
*/
public String getCredentialsSalt(){
return this.username+this.salt;
}
@Override
public String toString() {
return "UserInfo [uid=" + uid + ", username=" + username + ", name=" + name + ", password=" + password
+ ", salt=" + salt + ", state=" + state + "]";
}
}
sysrole
package com.bobby.springboot.entity;
/**
* Created by Bobby on 2019/9/19 17:23
*/
import java.io.Serializable;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
@Entity
@Table(name = "sys_role")
public class SysRole implements Serializable {
private static final long serialVersionUID = 1L;
@Id@GeneratedValue
private Long id; // 编号
private String role; // 角色标识程序中判断使用,如"admin",这个是唯一的:
private String description; // 角色描述,UI界面显示使用
private Boolean available = Boolean.FALSE; // 是否可用,如果不可用将不会添加给用户
//角色 -- 权限关系:多对多关系;
@ManyToMany(fetch= FetchType.EAGER)
@JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="permissionId")})
private List<SysPermission> permissions;
// 用户 - 角色关系定义;
@ManyToMany
@JoinTable(name="SysUserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="uid")})
private List<UserInfo> userInfos;// 一个角色对应多个用户
public List<UserInfo> getUserInfos() {
return userInfos;
}
public void setUserInfos(List<UserInfo> userInfos) {
this.userInfos = userInfos;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Boolean getAvailable() {
return available;
}
public void setAvailable(Boolean available) {
this.available = available;
}
public List<SysPermission> getPermissions() {
return permissions;
}
public void setPermissions(List<SysPermission> permissions) {
this.permissions = permissions;
}
// @Override
// public String toString() {
// return "SysRole [id=" + id + ", role=" + role + ", description=" + description + ", available=" + available
// + ", permissions=" + permissions + "]";
// }
}
@Controller
public class HomeController {
@RequestMapping({"/","/index"})
public String index() {
return "index";
}
@RequestMapping("/login")
public String login() {
return "login";
}
}
启动项目,数据库表会自动建好,index和login页面也可以访问。
(二)身份校验和角色设置
2.1 数据库中插入数据
INSERT INTO test.sys_permission VALUES ('1', 1, '用户管理', '0', '0/', 'userInfo:view', 'menu', 'userInfo/userList');
INSERT INTO test.sys_permission VALUES ('2', 1, '用户添加', '1', '0/1', 'userInfo:add', 'button', 'userInfo/userAdd');
INSERT INTO test.sys_permission VALUES ('3', 1, '用户删除', '1', '0/1', 'userInfo:del', 'button', 'userInfo/userDel');
INSERT INTO test.sys_role VALUES ('1', 1, '管理员', 'admin');
INSERT INTO test.sys_role VALUES ('2', 1, 'VIP会员', 'vip');
INSERT INTO test.user_info (uid,username,name,password,salt,state) VALUES ('1', 'admin','管理员' , 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 0);
INSERT INTO test.sys_role_permission VALUES ('1', '1');
INSERT INTO test.sys_role_permission VALUES ('1', '2');
INSERT INTO test.sys_user_role VALUES ('1', '1');
2.2 .Realms
Realm是一个Dao,通过它来验证用户身份和权限。这里Shiro不做权限的管理工作,需要我们自己管理用户权限,只需要从我们的数据源中把用户和用户的角色权限信息取出来交给Shiro即可。
在config
包下再建一个包Shiro
,然后在Shiro包下建一个MyShiroRealm
类,继承AuthorizingRealm抽象类。
package com.bobby.springboot.config;
import com.bobby.springboot.entity.SysPermission;
import com.bobby.springboot.entity.SysRole;
import com.bobby.springboot.entity.UserInfo;
import com.bobby.springboot.service.UserInforService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Created by Bobby on 2019/9/23 14:14
*/
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private UserInforService userService;
// 配置用户权限 访问页面的时 链接配置了相应的权限或者是shiro标签才能执行此方法
// 否则不执行
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("开始配置权限");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
UserInfo userInfo = (UserInfo)principalCollection.getPrimaryPrincipal();
for (SysRole role : userInfo.getRoleList()){
// 添加 角色
info.addRole(role.getRole());
for (SysPermission p : role.getPermissions()){
// 添加权限
info.addStringPermission(p.getPermission());
}
}
return info;
}
// 用户登录身份校验
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("开始身份校验");
String userName = (String) authenticationToken.getPrincipal();
UserInfo userInfo = userService.findByUsername(userName);
if(null == userInfo){
// //没有返回登录用户名对应的SimpleAuthenticationInfo对象时,
// 就会在LoginController中抛出UnknownAccountException异常
return null;
}
//验证通过返回一个封装了用户信息的AuthenticationInfo实例即可。
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userInfo,//用户信息
userInfo.getPassword(),//密码
getName()//realm name
);
// 设置盐
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(userInfo.getCredentialsSalt()));
return authenticationInfo;
}
}
重载以上两个方法来配置用户身份验证和权限验证。
别忘了在Service包下新建个UserInfoService和它的实现类:
2.3 service方法实现
/**
* Created by Bobby on 2019/9/23 14:18
*/
public interface UserInforService {
public UserInfo findByUsername(String username);
}
@Service
public class UserInfoServiceImpl implements UserInfoService {
@Resource
private UserInfoRepository userInfoRepository;
@Override
public UserInfo findByUsername(String username) {
System.out.println("UserInfoServiceImpl.findByUsername");
return userInfoRepository.findByUsername(username);
}
}
2.4 dao
/**
* Created by Bobby on 2019/9/23 16:27
*/
public interface UserInfoRepository extends CrudRepository<UserInfo, Long> {
public UserInfo findByUsername(String username);
public UserInfo save(UserInfo userInfo);
}
2.5 接下来配置Shiro的关键部分
这里要配置的是ShiroConfig类,Apache Shiro 核心通过 Filter 来实现,就好像SpringMvc 通过DispachServlet 来主控制一样。 既然是使用 Filter 一般也就能猜到,是通过URL规则来进行过滤和权限校验,所以我们需要定义一系列关于URL的规则和访问权限
/**
* Created by Bobby on 2019/9/23 14:46
*/
@Configuration
public class ShiroConfiguration {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String,String> filterChainDefinitionMap = new LinkedHashMap();
filterChainDefinitionMap.put("/logout","logout");
filterChainDefinitionMap.put("/**","authc");
filterChainDefinitionMap.put("/favicon.ico","anon");
//authc 表示需要验证身份证才能访问 ,还有一些比如anon表示不需要验证身份就能访问等。
shiroFilterFactoryBean.setLoginUrl("/login");
shiroFilterFactoryBean.setSuccessUrl("/index");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
//SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
@Bean
public MyShiroRealm myShiroRealm(){
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
//因为我们的密码是加过密的,所以,如果要Shiro验证用户身份的话,
// 需要告诉它我们用的是md5加密的,并且是加密了两次。同时我们在自己的Realm中也通过SimpleAuthenticationInfo返回了加密时使用的盐。
// 这样Shiro就能顺利的解密密码并验证用户名和密码是否正确了。
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
return hashedCredentialsMatcher;
}
关于为什么设置filterChainDefinitionMap.put("/favicon.ico", "anon");
请参考Shiro登录后下载favicon.ico问题
2.6 修改我们的HomeController中的/login
请求
@RequestMapping("/login")
public String login(HttpServletRequest request, Map<String, Object> map) {
// 登录失败从request中获取shiro处理的异常信息。
// shiroLoginFailure:就是shiro异常类的全类名.
Object exception = request.getAttribute("shiroLoginFailure");
String msg = "";
if (exception != null) {
if (UnknownAccountException.class.getName().equals(exception)) {
System.out.println("账户不存在");
msg = "账户不存在或密码不正确";
} else if (IncorrectCredentialsException.class.getName().equals(exception)) {
System.out.println("密码不正确");
msg = "账户不存在或密码不正确";
} else {
System.out.println("其他异常");
msg = "其他异常";
}
}
map.put("msg", msg);
// 此方法不处理登录成功,由shiro进行处理.
return "login";
}
这里@RequestMapping
之所以没加method是因为如果用户没登录,Shiro会调用get方法请求/login
,而后面我们在login页面会用post请求发送form表单,所以这里就没设置method(默认支持所有请求)。
好,启动项目。这时候我们再访问http://localhost:8080/index
会跳转到登录页面,因为我们设置了filterChainDefinitionMap.put("/**", "authc");
,所以要先验证身份才能访问。同时因为设置了shiroFilterFactoryBean.setLoginUrl("/login");
,所以会跳转到登录页面。这时候在登录页面输入正确的用户名密码就可以登录了,登录成功会自动跳转到index页面。
(三)权限 (精确到按钮级别的权限控制)
3.1在以上的基础上,添加一个UserInfoController
package com.bobby.springboot.controller;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Created by Bobby on 2019/9/23 17:00
*/
@Controller
@RequestMapping("/userinfor")
public class UserInfoController {
/**
* 用户查询.
* @return
*/
@RequestMapping("/userList")
@RequiresPermissions("userInfo:view")
public String userInfo(){
return "userInfo";
}
/**
* 用户添加;
* @return
*/
@RequestMapping("/userAdd")
@RequiresPermissions("userInfo:add")
public String userInfoAdd(){
return "userAdd";
}
@RequestMapping("/userDel")
@RequiresPermissions("userInfo:del")
public String userInfoDel() {
return "userDel";
}
}
这时候启动项目,上面的几个请求在登录之后我们都是可以访问的,并且不会执行doGetAuthorizationInfo()
打印权限验证信息。如果我们要限制某些人必须有一定的权限才能访问,怎么办呢?
3.2 开启Shiro AOP注解支持
ShiroConfiguration
中加入以下代码开启:
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
3.3然后,在controller中加入相应的注解即可:
/**
* 用户查询.
* @return
*/
@RequestMapping("/userList")
@RequiresPermissions("userInfo:view")
public String userInfo(){
return "userInfo";
}
/**
* 用户添加;
* @return
*/
@RequestMapping("/userAdd")
@RequiresPermissions("userInfo:add")
public String userInfoAdd(){
return "userAdd";
}
@RequestMapping("/userDel")
@RequiresPermissions("userInfo:del")
public String userInfoDel() {
return "userDel";
}
这时候,我们再登录之后访问http://localhost:8080/userInfo/userDel
就会报org.apache.shiro.authz.AuthorizationException
异常了。同时后台会打印权限验证的信息。
关于怎么处理这个没权限的异常,如果是在ShiroConfiguration
中配置403
是不起作用的,具体请参考setUnauthorizedUrl("/403")不起作用.
说下,按钮级别的权限控制,在我们的项目中,我们使用的Beetl这个前段模板+layui.js(吐槽下,真的不是很好用喃,做数据渲染的时候),在显示的过程中,会在模板中判断是否有权限,如同@RequiresPermissions("userInfo:add")这个教研的效果是一样的,通过访问用户是否有该权限字段,来判断按钮牛是否显示,这样做到按钮级别的权限控制。
上面所有的代码参考 : 工程参考 springboot整合shiro 完整版