GitHub: 代码连接.
https://github.com/17639622607/springshiro
前言
本项目中,实现权限控制一共用到了5张表,user(用户表)、role(角色表)、menu(资源表)。由于用户和角色之间、角色和资源之间存在多对多的关系,所以把三个表之间的关联信息抽离到两个表里。user_role(用户角色表)、role_menu(角色资源表)
用户表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户表',
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '密码',
`salt` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '盐值',
`enabled` int(10) DEFAULT 0 COMMENT '是否启用',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
角色表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT '角色表',
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '名称',
`remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '描述',
`role_sort` int(10) DEFAULT NULL COMMENT '排序',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
资源表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`menu_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
`menu_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '菜单名称',
`parent_id` int(11) DEFAULT 0 COMMENT '父菜单ID',
`order_num` int(4) DEFAULT 0 COMMENT '显示顺序',
`url` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '#' COMMENT '请求地址',
`menu_type` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
`visible` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
`perms` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '#' COMMENT '菜单图标',
`remark` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '备注',
PRIMARY KEY (`menu_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜单权限表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
用户角色表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT '用户角色表',
`user_id` int(10) DEFAULT NULL COMMENT '用户id',
`role_id` int(10) DEFAULT NULL COMMENT '角色id',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `user_id`(`user_id`, `role_id`) USING BTREE COMMENT '联合唯一键'
) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
角色资源表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT '角色资源表',
`role_id` int(10) DEFAULT NULL COMMENT '角色id',
`menu_id` int(10) DEFAULT NULL COMMENT '列表id',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `role_id`(`role_id`, `menu_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 190 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
先讲下user表,单纯的只对我们的密码加密往往是不够安全的,一般的话我们会拼接一个盐值。在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”
接下来我们来开发登录界面
login.html
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
<style>
body{
height: 100%;
background: url("../img/loginbg.jpg") no-repeat center fixed;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
color: rgba(255, 255, 255, .95);
}
form{
width: 240px;
height: 240px;
padding: 30px;
margin: 10% 0 0 63% ;
background: rgba(255,255,255,.2);
border: 1px solid rgba(255,255,255,.3);
border-radius: 3px;
}
input{
display: inline-block;
width: 225px;
height: 30px;
border-radius: 3px;
margin: 15px 0;
outline-style: none ;
border: none ;
text-indent: 10px;
}
#btnSubmit{
background-color: #1c84c6;
border-color: #1c84c6;
color: #FFFFFF;
cursor:pointer;
}
</style>
<link rel="shortcut icon" href="../static/favicon.ico" th:href="@{favicon.ico}"/>
</head>
<body>
<form>
登录
<input type="text" placeholder="用户名" name="username" id="username">
<input type="password" placeholder="密码" name="password" id="password">
<input type="button" value="登录" id="btnSubmit">
</form>
</body>
<script th:src="@{/layui/layui.js}"></script>
<script>
layui.use('layer', function(){
var $ = layui.jquery, layer = layui.layer;
$("#btnSubmit").click(function (){
$.ajax({
type: "post",
url: "/login",
data: {
username: $("#username").val(),
password: $("#password").val(),
},
dataType: "json",
success: function (res) {
if(res.code==0){
location.href = '/index';
}else {
layer.msg(res.msg);
}
}
});
})
})
</script>
</html>
在controller层加上映射
@GetMapping("/login")
public String login(){
return "login";
}
此时我们就有登录界面了,但是我们还没用户信息,下面我们来添加一个用户,
后端用户相关的开发
(一)添加用户实体类SysUser,类名用数据库表名,将首字母和下划线下一个字母大写
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.*;
import javax.persistence.Entity;
import javax.persistence.*;
@Data
@ToString
@Entity
@Table
@JsonIgnoreProperties({"handler","hibernateLazyInitializer"})
public class SysUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
private String username;
private String password;
private String salt;
private Integer enabled;
}
用JPA的话实体类简单的写法大概就这么写,是不是看着很复杂,不用每个注释都非常了解,大概知道干嘛的就行。这里的复杂为后面不用写sql语句稍微铺垫了一下
(二)UserDao层
import com.ysc.springshiro.entity.SysUser;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserDao extends JpaRepository<SysUser, Integer> {
//按照名字查找user
public SysUser findSysUserByUsername(String username);
}
继承JpaRepository类的话,里面已经包含了基本的增删改查sql语句,如果在不连表的情况下,我们可以直接写接口,这里大概讲一下吧
比如我们想按姓名查找SysUser对象的话,我们的方法名就叫可以写成findSysUserByUsername,参数写成字符串型,返回值是SysUser
如果我们想按Id查找的话,方法名则是findSysUserById,参数是整形,返回值是SysUser
(三)UserService层
import com.ysc.springshiro.dao.UserDao;
import com.ysc.springshiro.entity.SysUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserDao userDao;
//添加或者编辑user
public void insertUser(SysUser user){
//save() 方法的作用是,当主键存在时更新数据,当主键不存在时插入数据。
userDao.save(user);
}
//查询集合
public List<SysUser> findAllUser(){
return userDao.findAll();
}
//按照名字查找用户
public SysUser findUserByUsername(String username){
return userDao.findSysUserByUsername(username);
}
}
这里的UserService调用UserDao里面的方法,刚才UserDao里怎么写我们想要的方法已经大概说了下,接下来我们说下UserDao里自带的一些方法
查询集合:findAllUser
删除:delete
添加和修改是一个方法:save
方法我们是写好了,下面我们的目的是给我们这个项目添加一个用户,我们用户表中有个salt字段不知道大家玩了没,现在他要排上用场了。首先呢,由于salt这个字段是为了加密用的,这里需要引入我们的shiro框架,用里面的一些加密方法
(四)引入shiro
1. pom.xml中新加
<!--Shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.2</version>
</dependency>
<!-- shiro ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
<!-- pagehelper 分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
2.新建ShiroRealm和ShiroConfiguration
ShiroRealm
import com.ysc.springshiro.entity.SysUser;
import com.ysc.springshiro.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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/*** 授权*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//能进入到这里,表示账号已经通过验证了
SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
return s;
}
/*主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取账号密码
UsernamePasswordToken t = (UsernamePasswordToken) token;
String username = token.getPrincipal().toString();
SysUser user=userService.findUserByUsername(username);
//获取数据库中的密码
String passwordInDB = user.getPassword();
//认证信息里存放账号密码,getName()是当前Reaml的继承方法,通常返回当前类名 :shirorealm
String salt = user.getSalt();
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username,passwordInDB, ByteSource.Util.bytes(salt),getName());
return authenticationInfo;
}
}
ShiroConfiguration
@Configuration
public class ShiroConfiguration {
@Bean
public static LifecycleBeanPostProcessor getLifecycleBeanProcessor() {
return new LifecycleBeanPostProcessor();
}
//配置核心安全事务管理器
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(getBootRealm());
return securityManager;
}
//配置自定义的权限登录器
@Bean
public ShiroRealm getBootRealm() {
ShiroRealm wjRealm = new ShiroRealm();
wjRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return wjRealm;
}
//自定义密码加密规则
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
/**
* 开启aop注解支持
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager manager) {
ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
bean.setSecurityManager(manager);
//配置登录的url和登录成功的url以及验证失败的url
bean.setLoginUrl("/login");
bean.setSuccessUrl("/index");
//配置访问权限
LinkedHashMap<String, String> filterChainDefinitionMap=new LinkedHashMap<>();
filterChainDefinitionMap.put("/login/*", "anon");
filterChainDefinitionMap.put("/static/*", "anon");
//所有的路径都拦截,被UserFilter拦截,这里会判断用户有没有登陆
filterChainDefinitionMap.put("/*", "user");
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return bean;
}
}
3.调用接口添加一个用户
@Controller
public class TestController {
@Autowired
UserService userService;
@GetMapping("/register")
@ResponseBody
public String register(String username, String password){
SysUser user=new SysUser();
String salt = new SecureRandomNumberGenerator().nextBytes().toString();// 生成盐,默认长度 16 位
int times = 2;// 设置 hash 算法迭代次数
user.setUsername(username);
user.setSalt(salt);
String encodedPassword = new SimpleHash("md5", password, salt, times).toString();// 得到 hash 后的密码
user.setPassword(encodedPassword);
try{
userService.insertUser(user);
return "成功";
}catch (Exception e){
return "失败";
}
}
}
我们在浏览器地址手动调这个接口,如下操作
这时我们看数据库,数据已经添加上了
5.写登录逻辑
在写逻辑之前,小伙伴们先把一些公共的方法引进来,可以先把我git上的项目拉下来,这些公共的方法也都是以前封装的,这里就不做一一介绍了,都不是很难理解
登录逻辑
@Controller
public class LoginController extends BaseController {
@Autowired
UserService userService;
@GetMapping("/login")
public String login(){
return "login";
}
@RequestMapping("/login")
@ResponseBody
public AjaxResult ajaxLogin(String username, String password) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
return success();
}catch (AuthenticationException e){
return error("用户或密码错误");
}
}
@GetMapping("/logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "login";
}
}
这里有的小伙伴可能就要问了,不是要和数据库的用户做对比吗,怎么没调数据库呢。这里我们调用subject.login(token)已经调用了ShiroRealm中我们自定义的登录方法doGetAuthenticationInfo,这样我们的登录功能就写好了