通过上一篇Shiro基础教程 https://blog.csdn.net/I_No_dream/article/details/92799372相信大家已经能够完成简单的认证了. 接下来我们分析以下Shiro的核心API
分析Shiro的核心API
- Suject: 用户主体(认证, 授权等方法)
- SecurityManager: 安全管理器
- Realm: Shiro连接数据库的桥梁
这三个核心的API他们存在一种关系, Subject关联SecurityManager, 把操作交给SecurityManager, SecurityManager关联Realm, 将认证信息或授权信息进行与数据库或缓存的连接认证
目录
pom.xml文件
<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-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
yml文件
spring:
datasource:
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
mybatis:
configuration:
auto-mapping-behavior: full
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.lijiajun.entity
common下的shiro包
shiroConfig类
@Configuration
public class ShiroConfig {
/**
* 创建ShiroFilterFactoryBean
*/
@Bean(name = "factoryBean")
public ShiroFilterFactoryBean shirFilter(@Qualifier("securityManager")DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
//设置安全管理 这个属性是必须的。
factoryBean.setSecurityManager(securityManager);
/*
Shiro内置过滤器, 可以实现权限相关的拦截器
常用的过滤器:
anon:表示可以匿名使用。
authc:表示需要认证(登录)才能使用,没有参数
roles:参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"]
每个参数通过才算通过,相当于hasAllRoles()方法。
perms:参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"]
当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
rest:根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。
port:当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等
serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。
authcBasic:没有参数表示httpBasic认证
ssl:表示安全的url请求,协议为https
user:当登入操作时不做检查
*/
// 有序map
Map<String,String> filterMap = new LinkedHashMap<>();
// 拦截请求,未登录不可访问
filterMap.put("/userDelete","authc");
filterMap.put("/userAdd","authc");
filterMap.put("/userInfo","authc");
// 放行请求
// filterMap.put("/test","anon");
// filterMap.put("/login","anon");
//授权过滤器 注意: 当授权拦截就, shiro会自动跳转到未授权的页面
// filterMap.put("/add","perms[user:add]");
// filterMap.put("/update","perms[user:update]");
// 登出操作
filterMap.put("/logout", "logout");
// 统一过滤 注意 这个需要写在最后
// filterMap.put("/*","authc");
/*
loginUrl :没有登录的用户请求需要登录的页面时自动跳转到登录页面,不是必须的属性
不输入地址的话会自动寻找项目web项目的根目录下的”/login.jsp”页面。
successUrl :登录成功默认跳转页面,不配置则跳转至”/”。如果登陆前点击的一个需要登录的页面
则在登录自动跳转到那个需要登录的页面。不跳转到此。
unauthorizedUrl :没有权限默认跳转的页面
*/
factoryBean.setLoginUrl("/toLogin");
factoryBean.setSuccessUrl("/test");
// 此处因为类型原因,所以自定义了一个类来处理未授权跳转的页面 MyException
// factoryBean.setUnauthorizedUrl("/noAuth");
factoryBean.setFilterChainDefinitionMap(filterMap);
return factoryBean;
}
/**
* 创建Realm
*/
@Bean(name = "userRealm")
public UserRealm getRealm(){
return new UserRealm();
}
/**
* 创建DefaultWebSecurityManager
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联realm
securityManager.setRealm(userRealm);
return securityManager;
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启aop注解支持
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
UserRealm类
public class UserRealm extends AuthorizingRealm {
/**
* 用户
*/
@Autowired
private UserService userService;
/**
* 角色
*/
@Autowired
private RolesService rolesService;
/**
* 权限
*/
@Autowired
private PermissionsService permissionsService;
/**
* 权限验证
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 能进入到这里,表示账号已经通过验证了
String username = (String) principals.getPrimaryPrincipal();
User user = userService.selByNameUser(username);
//获取用户所有角色信息
Set<String> roleString = rolesService.selByUserIdString(user.getId());
//查询该用户所拥有的角色
List<Roles> roleList = rolesService.selByUserIdRoles(user.getId());
//查询所有角色所拥有的权限, 并保存
Set<String> perms = new HashSet<>();
for(Roles role : roleList) {
Set<String> permString = permissionsService.selByRoleIdPermission(role.getId());
perms.addAll(permString);
}
// 授权对象
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 把通过数据库获取到的权限保存到shiro
info.setStringPermissions(perms);
//把通过数据库获取到的角色保存到shiro
info.setRoles(roleString);
return info;
}
/**
* 身份认证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取登陆的用户名和密码
UsernamePasswordToken t = (UsernamePasswordToken) token;
String username = token.getPrincipal().toString();
String password = new String(t.getPassword());
//从数据库中获取登陆的用户
User user = userService.selByNameUser(username);
//没找到用户
if(null==user) {
throw new UnknownAccountException();
}
//把用户输入的密码进行md5和盐加密一起运算1次
String encodedPassword = new SimpleHash("md5", password, user.getUsername(), 1).toString();
//上一步加密后的密文和数据库的密文进行判断
if (!user.getPassword().equals(encodedPassword)) {
//密码出错
throw new IncorrectCredentialsException();
}
// 认证信息里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名 :databaseRealm
SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(username, password, getName());
//清缓存中的授权信息,保证每次登陆 都可以重新授权。因为AuthorizingRealm会先检查缓存有没有 授权信息,再调用授权方法
super.clearCachedAuthorizationInfo(a.getPrincipals());
return a;
}
}
MyException类 在shiroConfig类中已经说明了其作用
@ControllerAdvice
public class MyException {
/**
* 未授权跳转
* @return
*/
@ExceptionHandler(AuthorizationException.class)
public String error() {
return "noAuth";
}
//
// /**
// * 未认证跳转
// * @return
// */
// @ExceptionHandler(UnauthorizedException.class)
// public String noLogin(){
// return "login";
// }
}
common下的util包
SetStringUtil类 将查询到的数据以set集合保存
public class SetStringUtil {
/**
* 查询用户所拥有的角色并转换为字符串,用作shiro中的角色权限
* @param list
* @return
*/
public static Set<String> listRoleConversion(List<Roles> list){
Set<String> set = new HashSet<>();
for(Roles role : list) {
set.add(role.getRoleName());
}
return set;
}
/**
* 查询角色所拥有的权限并转换为字符串,用作shiro中的角色权限
* @param list
* @return
*/
public static Set<String> listPermissionConversion(List<Permissions> list){
Set<String> set = new HashSet<>();
for(Permissions permission : list) {
set.add(permission.getPermissionName());
}
return set;
}
}
entity包(实体类)
用户
@Data
public class User implements Serializable {
private static final long serialVersionUID = 8974323100336176485L;
private Integer id;
private String username;
private String password;
}
角色
@Data
public class Roles implements Serializable {
private static final long serialVersionUID = 6341445277560423251L;
private Integer id;
private Integer userId;
private String roleName;
}
权限
@Data
public class Permissions implements Serializable {
private static final long serialVersionUID = 6564673599295382095L;
private Integer id;
private Integer roleId;
private String permissionName;
}
mapper层
接口就不展示出来了, 我将mapper.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.lijiajun.mapper.UserMapper">
<!-- 结果映射 -->
<resultMap id="usersMap" type="com.lijiajun.entity.User">
<id column="id" property="id" jdbcType="INTEGER" />
<result column="username" property="username" jdbcType="VARCHAR" />
<result column="password" property="password" jdbcType="VARCHAR" />
</resultMap>
<select id="selByNameUser" resultMap="usersMap">
select * from users where username = #{username}
</select>
</mapper>
角色
<?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.lijiajun.mapper.RolesMapper">
<!-- 结果映射 -->
<resultMap id="userRolesMap" type="com.lijiajun.entity.Roles">
<id column="id" property="id" jdbcType="INTEGER" />
<result column="user_id" property="userId" jdbcType="INTEGER" />
<result column="role_name" property="roleName" jdbcType="VARCHAR" />
</resultMap>
<select id="selByUserIdRoles" resultMap="userRolesMap">
select * from user_roles where user_id = #{userId}
</select>
</mapper>
权限
<?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.lijiajun.mapper.PermissionMapper">
<!-- 结果映射 -->
<resultMap id="rolesPermissionsMap" type="com.lijiajun.entity.Permissions">
<id column="id" property="id" jdbcType="INTEGER" />
<result column="permission_name" property="permissionName" jdbcType="VARCHAR" />
<result column="role_id" property="roleId" jdbcType="INTEGER" />
</resultMap>
<select id="selByRoleIdPermission" resultMap="rolesPermissionsMap">
select * from roles_permissions where role_id = #{roleId}
</select>
</mapper>
service层
同样不贴出接口,只将实现类贴出
用户
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
/**
* 根据用户名和密码进行数据库数据匹配
* @param username
* @return 返回一个相应于的用户
*/
@Override
public User selByNameUser(String username) {
return userMapper.selByNameUser(username);
}
}
角色
@Service
public class RolesServiceImpl implements RolesService {
@Autowired
private RolesMapper rolesMapper;
/**
* 查询用户的角色, 此信息用于查询角色的权限
*
* @param id
* @return
*/
@Override
public List<Roles> selByUserIdRoles(Integer id) {
return rolesMapper.selByUserIdRoles(id);
}
/**
* 查询用户所拥有的角色并转换为字符串,用作shiro中的角色权限
*
* @param userId
* @return
*/
@Override
public Set<String> selByUserIdString(Integer userId) {
List<Roles> roles = rolesMapper.selByUserIdRoles(userId);
return SetStringUtil.listRoleConversion(roles);
}
}
权限
@Service
public class PermissionsServiceImpl implements PermissionsService {
@Autowired
private PermissionMapper permissionMapper;
/**
* 查询角色所拥有的权限并转换为字符串,用作shiro中的角色权限
* @param roleId
* @return
*/
@Override
public Set<String> selByRoleIdPermission(Integer roleId) {
List<Permissions> permissions = permissionMapper.selByRoleIdPermission(roleId);
return SetStringUtil.listPermissionConversion(permissions);
}
}
controller层
@Controller
public class IndexController {
/**
* 访问用户列表页面
* @return
*/
@RequestMapping("/userInfo")
@RequiresPermissions("user:select")//我的用户并没有这个权限,所以访问不了
public String userInfo() {
return "userInfo";
}
/**
* 访问用户增加页面
* @return
*/
@RequestMapping("/userAdd")
@RequiresRoles("admins")//我的用户并没有admin这个角色,所以访问不了
public String userAdd() {
return "userAdd";
}
/**
* 访问用户删除页面
* @return
*/
@RequestMapping("/userDelete")
@RequiresPermissions("user:delete")
public String userDelete() {
return "userDelete";
}
}
@Controller
public class UserController {
/**
* 登陆逻辑
* @param
* @return
*/
@RequestMapping("/login")
public String login(String username, String password, Model model){
//1.获取Subject
Subject subject = SecurityUtils.getSubject();
//2.封装用户数据
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
//3.执行登录方法
try {
//没发生异常代表登录成功 只要发生异常就代表登陆失败
subject.login(token);
return "redirect:/test";
} catch (UnknownAccountException e) {
//此异常为用户名不存在
//e.printStackTrace();
model.addAttribute("msg","用户名不存在");
return "/login";
}catch (IncorrectCredentialsException e){
//此异常为密码错误
model.addAttribute("msg","密码错误");
return "/login";
}
}
/**
* 跳转到登录页面
* @return
*/
@RequestMapping("/toLogin")
public String toLogin(){
return "/login";
}
/**
* 登出
* @return
*/
@RequestMapping("/logouts")
@ResponseBody
public String logout(){
return "退出成功";
}
/**
* 跳转到用户登陆页面
* @return
*/
@RequestMapping("/test")
public String test(){
return "/test";
}
}
html页面
登录页
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2 th:text="${msg}"></h2>
<form action="/login" method="post">
用户名:<input type="text" name="username" />
密码:<input type="text" name="password" />
<button type="submit">登陆</button>
</form>
</body>
</html>
功能页
<!DOCTYPE html>
<html lang="en" xmlns:shiro="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>测试</title>
</head>
<body>
<div>进入用户列表功能: <a href="/userInfo">用户列表</a></div>
<div>进入用户新增功能: <a href="/userAdd">用户新增</a></div>
<div>进入用户删除功能: <a href="/userDelete">用户删除</a></div>
<a href="/logouts">退出</a>
</body>
</html>
未授权提示页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>未授权提示页面</title>
</head>
<body>
你没有权限访问
</body>
</html>
用户列表页
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>用户信息列表</h1>
</body>
</html>
用户新增页
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>添加用户</h1>
</body>
</html>
用户删除页
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>删除用户</h1>
</body>
</html>