shiro是什么呢?其实简单来说,shiro就是一个java的安全框架,执行身份验证、授权、密码和会话管理。
这里需要重点了解一下shiro核心的API:Subject、SecurityManager、Realm
Subject:用户主体(把操作交给SecurityMangager)
SecurityManager:安全管理器(关联Realm)
Realm:Shiro连接数据的桥梁
会不多说,实践为主。
该demo使用的技术栈是SpringBoot+MyBatis+shiro,数据库是使用MySQL数据库,通过Redis进行缓存和Session会话管理,风格的话是采用restful风格,使用swagger2测试,如果不习惯的话也可以直接postman就行。
注意:这里做前后端分离的
1. 导入依赖
pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</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>1.1.9</version>
</dependency>
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!-- 虽然是前后端分离,但是为了登录页面可以测试,加上thymeleaf依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2. 配置文件 application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/xxxxxx?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Hongkong
username: xxxx
password: xxxxxx
#使用阿里的druid数据源
type: com.alibaba.druid.pool.DruidDataSource
mybatis:
#实体类对应的包路径
type-aliases-package: com.bgy.entity
mapper-locations: classpath:mapper/*.xml
configuration:
use-generated-keys: true
#驼峰命名
map-underscore-to-camel-case: true
#使用类的别名
use-column-label: true
3. shiro配置类
shiroConfig.class:
@Configuration
public class ShiroConfig {
/**
* 创建ShiroFilterFactoryBean
*/
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//添加Shiro内置过滤器
/**
* Shiro内置过滤器,可以实现权限相关的拦截器
* 常用的过滤器:
* anon: 无需认证(登录)可以访问
* authc: 必须认证才可以访问
* user: 如果使用rememberMe的功能可以直接访问
* perms: 该资源必须得到资源权限才可以访问
* role: 该资源必须得到角色权限才可以访问
*/
Map<String, String> filerMap = new LinkedHashMap<>();
filerMap.put("/login", "anon");
filerMap.put("/encrypt/*", "anon");
// 添加页面需要的权限
filerMap.put("/index", "perms[sys:editor]");
// 全部拦截
filerMap.put("/**", "authc");
// 修改调整的登录页面
shiroFilterFactoryBean.setLoginUrl("/toLogin");
// 登录成功后跳转的页面
shiroFilterFactoryBean.setSuccessUrl("/index");
// 设置未授权提示页面
shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filerMap);
return shiroFilterFactoryBean;
}
/**
* 创建DefaultWebSecurityManager
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(userRealm);
return defaultWebSecurityManager;
}
/**
* 创建Realm
*/
@Bean(name = "userRealm")
public UserRealm getRealm() {
return new UserRealm();
}
}
自定义Realm类,UserRealm.class :
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 执行授权逻辑
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获取当前登录用户
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getPrincipal();
User dbUser = userService.getUserInfoByName(user.getUsername());
RolePermission rolePermission = userService.getPermIdByRoleId(dbUser.getRoleId());
Permission permission = userService.getPermDetailByPermId(rolePermission.getPermId());
System.out.println("AuthorizationInfo->" + permission.getPermDetail());
//为用户添加权限
info.addStringPermission(permission.getPermDetail());
return info;
}
/**
* 执行认证逻辑
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws
AuthenticationException {
//判断用户名
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
User user = userService.getUserInfoByName(token.getUsername());
if (user == null) {
System.out.println(("用户名不存在"));
//用户名不存在
//shiro底层会抛出UnKnowAccountException
return null;
}
//判断密码
try {
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), "");
return simpleAuthenticationInfo;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
这里数据是连接数据库的,如果不连接的,可以直接静态模拟数据测试。
4. Mybatis操作数据库
数据库表:
正常基于shiro框架是五张表,用户表、角色表、用户-角色表、权限表、角色-权限表,我们这里只做四表的,去掉用户-角色表
CREATE TABLE `t_user_info` (
`user_id` bigint(20) NOT NULL COMMENT '用户id',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(255) NOT NULL COMMENT '密码',
`role_id` int(11) DEFAULT NULL COMMENT '角色id',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户信息表';
CREATE TABLE `sys_roles` (
`role_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`role_name` varchar(20) NOT NULL COMMENT '角色名称',
PRIMARY KEY (`role_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
CREATE TABLE `sys_permissions` (
`perm_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '权限ID',
`perm_name` varchar(255) NOT NULL COMMENT '权限名称',
`perm_detail` varchar(20) NOT NULL COMMENT '权限对应代码',
PRIMARY KEY (`perm_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
CREATE TABLE `sys_roles_permissions` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '表自增ID',
`role_id` int(11) NOT NULL COMMENT '角色ID',
`perm_id` int(11) NOT NULL COMMENT '权限ID',
PRIMARY KEY (`id`) USING BTREE,
KEY `perm_id` (`perm_id`),
KEY `role_id` (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
实体类entity:
User.class:
@Data
@ApiModel
public class User {
private long userId;
private String username;
private String password;
private int roleId;
}
Role.class:
@Data
@ApiModel
public class Role {
private int roleId;
private String roleName;
}
Permission.class:
@Data
@ApiModel
public class Permission {
private int permId;
private String permName;
private String permDetail;
}
RolePermission.class:
@Data
@ApiModel
public class RolePermission {
private int id;
private int roleId;
private int permId;
}
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.bgy.mapper.UserMapper">
<!-- 这里不通过join连接查询,通过多个select查询到perm_detail,即权限对应的代码 -->
<select id="getUserInfoByName" resultType="com.bgy.entity.User">
select user_id, username, password, role_id
from t_user_info
where username = #{username, jdbcType=VARCHAR}
</select>
<select id="getPermIdByRoleId" resultType="com.bgy.entity.RolePermission">
select id, role_id, perm_id from sys_roles_permissions
where role_id = #{role_id, jdbcType=INTEGER}
</select>
<select id="getPermDetailByPermId" resultType="com.bgy.entity.Permission">
select perm_id, perm_name, perm_detail from sys_permissions
where perm_id = #{perm_id, jdbcType=INTEGER}
</select>
<!-- 用于密码加密更新数据库 -->
<update id="updatePasswordWithEncryption" parameterType="string">
update t_user_info
set password = #{password}
where username = #{username}
</update>
</mapper>
数据库配置以及映射都准备好了,接下来当然是三层模型了。
UserMapper.class:
@Mapper
@Repository
public interface UserMapper {
/**
* 通过用户名查询用户信息
* @param username
* @return user
*/
public User getUserInfoByName(String username);
/**
* 根据角色id查询权限id
* @param role_id
* @return
*/
public RolePermission getPermIdByRoleId(int role_id);
/**
* 根据角色id查询权限detail
* @param perm_id
* @return
*/
public Permission getPermDetailByPermId(int perm_id);
/**
* 更新加密后的密码
* @param username
* @param password
* @return
*/
public int updatePasswordWithEncryption(String username, String password);
}
UserService.class:
public interface UserService {
/**
* 通过用户名查询用户信息
* @param username
* @return user
*/
public User getUserInfoByName(String username);
/**
* 根据角色id查询权限id
* @param role_id
* @return
*/
public RolePermission getPermIdByRoleId(int role_id);
/**
* 根据角色id查询权限detail
* @param perm_id
* @return
*/
public Permission getPermDetailByPermId(int perm_id);
/**
* 更新加密后的密码
* @param username
* @param password
* @return
*/
public int updatePasswordWithEncryption(String username, String password);
}
UserServiceImpl.class:
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Override
public User getUserInfoByName(String username) {
return userMapper.getUserInfoByName(username);
}
@Override
public RolePermission getPermIdByRoleId(int role_id) {
return userMapper.getPermIdByRoleId(role_id);
}
@Override
public Permission getPermDetailByPermId(int perm_id) {
return userMapper.getPermDetailByPermId(perm_id);
}
@Override
public int updatePasswordWithEncryption(String username, String password) {
return userMapper.updatePasswordWithEncryption(username, password);
}
}
UserController.class:
@Api(tags = "用户信息接口")
@Controller
public class UserInfoController {
@Resource
UserService userService;
@ControllerLog(description = "获取用户信息")
@ApiOperation(value = "获取用户信息")
@RequestMapping(value = "/user/{username}", method = RequestMethod.GET)
@ResponseBody
public Map<String, Object> getUserInfoById(@PathVariable String username) {
Map<String, Object> map = new HashMap<>(16);
User user = userService.getUserInfoByName(username);
map.put("username", user.getUsername());
map.put("roleId", user.getRoleId());
RolePermission rolePermission = userService.getPermIdByRoleId(user.getRoleId());
Permission permission = userService.getPermDetailByPermId(rolePermission.getPermId());
map.put("permDetail", permission.getPermDetail());
return map;
}
@ApiOperation(value = "拦截后返回登录页的接口")
@RequestMapping(value = "/toLogin", method = RequestMethod.GET)
public Object toLogin() {
Map<String, Object> map = new HashMap<>(16);
map.put("code", 200);
map.put("msg", "未登录");
return "/login";
}
@ApiOperation(value = "登录测试页面的接口")
@RequestMapping(value = "/login", method = RequestMethod.POST)
public Object login(@RequestParam String name, String password, Model model) {
// 获取Subject
Subject subject = SecurityUtils.getSubject();
// 封装用户数据
UsernamePasswordToken token = new UsernamePasswordToken(name, password);
// 执行登录方法
try {
subject.login(token);
return "redirect:/index";
} catch (UnknownAccountException e) {
// 登录失败:用户名不存在
model.addAttribute("msg", "用户名不存在");
return "/login";
} catch (IncorrectCredentialsException e) {
// 登录失败:密码错误
model.addAttribute("msg", "密码错误");
return "/login";
}
}
@ApiOperation("登录成功")
@RequestMapping(value = "/index", method = RequestMethod.GET)
@ResponseBody
public Object index() {
Map<String, Object> map = new HashMap<>(16);
map.put("code", 100);
map.put("msg", "您登录成功");
return map;
}
@ApiOperation(value = "未授权提示")
@RequestMapping(value = "/noAuth", method = RequestMethod.GET)
@ResponseBody
public Object noAuth() {
Map<String, Object> map = new HashMap<>(16);
map.put("code", 200);
map.put("msg", "您没有该授权");
return map;
}
}
5. 测试
测试只需一个login.html的测试页面即可,其他跳转直接以json打印出来就好。
login.html登录页面使用了thymeleaf模板引擎。
login.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<h3>登录</h3>
<h3 th:text="${msg}" style="color: red"></h3>
<form method="post" action="login">
用户名:<input type="text" name="name"/><br/>
密码:<input type="password" name="password"/><br/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
到这里,基本的拦截已经完成了。下一篇我们在用3DES加密为密码进行加密。
如果有什么需要改进的,还请多加指教。