- SpringBoot整合Shiro
- 1.springboot基本环境搭建
- (1)pom.xml依赖
- (2)webapp目录
- (3)application.yml配置jsp模板
- (4)Working directory目录
- (5)测试
- 2.集成shiro
- (1)pom.xml依赖
- (2)自定义Realm
- (3)ShiroConfiguration
- (4)login.jsp
- (5)index.jsp
- (6)测试
- 3.实现认证和退出
- (1)login.jsp实现登录
- (2)index.jsp实现退出
- (3)在CustomerRealm中实现认证
- (4)修改ShiroConfiguration
- (5)测试
- 4.MD5、Salt的注册流程
- 新建register.jsp
- 新建t_user
- 添加依赖
- 修改application.yml
- 新建User.java
- 新建UserMapper.java
- 新建UserService.java、UserServiceImpl.java及相关类
- 修改UserController.java
- 修改ShiroConfiguration.java
- 测试
- 5.MD5、Salt的认证流程
- 修改UserService.java、UserServiceImpl.java
- 修改Usermapper
- 修改CustomerRealm.java及其相关类
- 修改ShiroConfiguration.java及其相关类
- 测试
- 6.实现授权
- 1.基于角色的授权
- 2.基于权限的授权
- 7.EhCache实现缓存
- 修改pom.xml
- ShiroConfiguration.java
- ehcache.xml
- 8.集成Redis实现Shiro缓存
- 修改pom.xml
- RedisCacheManager.java
- RedisCache.java
- 编写ShiroByteSource
- 修改ShiroConfiguration.java
- 测试
- 9.集成图片验证码
- login.jsp
- 工具类-VerifyCodeUtil.java
- Controller层
- 修改ShiroConfiguration.java
本案例使用jsp作为前端页面展示形式,所以新建的springboot工程需要进行一下配置
(1)pom.xml依赖(2)webapp目录org.springframework.boot spring-boot-starter-web org.apache.tomcat.embed tomcat-embed-jasper jstl jstl 1.2 org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine
在main目录下我们新建一个webapp目录,目录里面新建一个jsp文件index.jsp
index
hello world!
这里webapp没有一个蓝色小圆圈,可以参考:SpringBoot中集成JSP如何创建webapp
(3)application.yml配置jsp模板spring:
# 设置视图模板为jsp
mvc:
view:
prefix: /
suffix: .jsp
(4)Working directory目录
(5)测试
2.集成shiro
(1)pom.xml依赖
(2)自定义Realmorg.apache.shiro shiro-core 1.4.0 org.apache.shiro shiro-spring 1.4.0 org.apache.shiro shiro-web 1.4.0
我们知道实际开发中使用shiro时都是使用自定的realm,我们先不管三七二十一,先自定义一个realm,暂时不实现认证和授权
package com.mye.hl21shrio.shiro.realm;
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.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class CustomerRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return null;
}
}
(3)ShiroConfiguration
这个类是Shiro的核心配置类,里面继承了ShiroFilter、SecurityManager和上面的自定义的Realm
package com.mye.hl21shrio.config;
import com.mye.hl21shrio.shiro.realm.CustomerRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class ShiroConfiguration {
//1.创建shiroFilter //负责拦截所有请求
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//配置系统受限资源
//配置系统公共资源
Map map = new HashMap();
map.put("/index.jsp","authc");//authc 请求这个资源需要认证和授权
//身份认证失败,则跳转到登录页面的配置 没有登录的用户请求需要登录的页面时自动跳转到登录页面,
// 不是必须的属性,不输入地址的话会自动寻找项目web项目的根目录下的”/login.jsp”页面。
shiroFilterFactoryBean.setLoginUrl("/login.jsp");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//2.创建安全管理器
@Bean(name = "defaultWebSecurityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//给安全管理器设置
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
//3.创建自定义realm
@Bean
public Realm getRealm(){
return new CustomerRealm();
}
}
(4)login.jsp
在上面的ShiroConfiguration类中,我们看到未认证的用户访问受限资源(这里指index.jsp)时会自动跳转到登录页面login.jsp,shiro默认的登录页面也是这个,我们在webapp下新建一个login.jsp
<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
index
用户登录
(5)index.jsp
与login.jsp区别开来,我们更改index.jsp的内容如下
<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
login
系统主页
- 用户管理
- 订单管理
这里访问127.0.0.1:8080/index.jsp跳转到127.0.0.1:8080/login.jsp则说明shiro配置成功
3.实现认证和退出 (1)login.jsp实现登录可以看到上面jsp的登录请求是/user/login
import org.apache.shiro.SecurityUtils;
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.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("user")
public class UserController {
@RequestMapping("login")
public String login(String username,String password){
// 获取当前登录用户
Subject subject = SecurityUtils.getSubject();
try {
// 执行登录操作
subject.login(new UsernamePasswordToken(username,password));
// 认证通过后直接跳转到index.jsp
return "redirect:/index.jsp";
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误~");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误~");
} catch (Exception e) {
e.printStackTrace();
}
// 如果认证失败仍然回到登录页面
return "redirect:/login.jsp";
}
}
(2)index.jsp实现退出
<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
index
系统主页
退出用户
- 用户管理
- 订单管理
对应的也要在controller添加方法
@RequestMapping("logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
// 退出后仍然会到登录页面
return "redirect:/login.jsp";
}
(3)在CustomerRealm中实现认证
package com.mye.hl21shrio.shiro.realm;
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.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class CustomerRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 在token中获取用户名
String principal = (String) token.getPrincipal();
System.out.println(principal);
// 模拟根据身份信息从数据库查询
if("christy".equals(principal)){
// 参数说明:用户名 | 密码 | 当前realm的名字
return new SimpleAuthenticationInfo(principal,"123456", this.getName());
}
return null;
}
}
(4)修改ShiroConfiguration
//配置系统受限资源 //配置系统公共资源 Map(5)测试 4.MD5、Salt的注册流程 新建register.jspmap = new HashMap (); map.put("/user/login","anon"); // anon 设置为公共资源,放行要注意anon和authc的顺序 map.put("/index.jsp","authc"); //authc 请求这个资源需要认证和授权 //默认认证界面路径 shiroFilterFactoryBean.setLoginUrl("/login.jsp"); shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
register
用户注册
新建t_user
DROp TABLE IF EXISTS `t_user`; create table `t_user` ( `id` int (11), `username` varchar (32), `password` varchar (32), `salt` varchar (32), `age` int (11), `email` varchar (32), `address` varchar (128) );添加依赖
com.baomidou
mybatis-plus-boot-starter
3.4.1
com.alibaba
druid-spring-boot-starter
1.1.10
mysql
mysql-connector-java
5.1.47
修改application.yml
spring:
# 设置视图模板为jsp
mvc:
view:
prefix: /
suffix: .jsp
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis_test?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=UTC
username: root
password: root
# 监控统计拦截的filters
filters: stat,wall,log4j,config
# 配置初始化大小/最小/最大
initial-size: 5
min-idle: 5
max-active: 20
# 获取连接等待超时时间
max-wait: 60000
# 间隔多久进行一次检测,检测需要关闭的空闲连接
time-between-eviction-runs-millis: 60000
# 一个连接在池中最小生存的时间
min-evictable-idle-time-millis: 300000
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
# 打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
mybatis-plus:
type-aliases-package: com.mye.hl21shrio.entity
configuration:
map-underscore-to-camel-case: true
新建User.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_user")
public class User implements Serializable {
@TableId(type = IdType.AUTO)
private Integer id;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String username;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String password;
@TableField(fill = FieldFill.INSERT)
private String salt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Integer age;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String email;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String address;
}
新建UserMapper.java
@Mapper public interface UserMapper extends baseMapper新建UserService.java、UserServiceImpl.java及相关类{ }
UserService.java
import com.mye.hl21shrio.entity.User;
public interface UserService {
void register(User user);
}
UserServiceImpl.java
package com.mye.hl21shrio.service;
import com.mye.hl21shrio.entity.User;
import com.mye.hl21shrio.global.ShiroConstant;
import com.mye.hl21shrio.mapper.UserMapper;
import com.mye.hl21shrio.utils.SaltUtil;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public void register(User user) {
// 生成随机盐
String salt = CryptoUtil.generateSalt();
// 保存随机盐
user.setSalt(salt);
// 生成密码
String encryptPassword = CryptoUtil.createEncryptPassword(user.getPassword(), salt);
user.setPassword(encryptPassword);
userMapper.insert(user);
}
}
CryptoUtil.java
public class CryptoUtil {
private static final RandomNumberGenerator RANDOM_NUMBER_GENERATOR = new SecureRandomNumberGenerator();
private static final String ALGORITHM_NAME = "md5";
private static final int HASH_ITERATIONS = 1024;
private static final String HMAC_SHA1 = "HmacSHA1";
public static String generateSalt() {
return RANDOM_NUMBER_GENERATOR.nextBytes().toHex();
}
public static String createEncryptPassword(String password, String salt) {
return new SimpleHash(ALGORITHM_NAME, password, ByteSource.Util.bytes(salt),
HASH_ITERATIONS).toHex();
}
public static void encryptPassword(User user) {
String newPassword = new SimpleHash(
ALGORITHM_NAME,
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()),
HASH_ITERATIONS).toHex();
user.setPassword(newPassword);
}
public static String base64Encode(String s) {
if (StringUtils.isBlank(s)) {
return null;
}
return (new sun.misc.base64Encoder()).encode(s.getBytes());
}
public static String hmacSHA1Encode(String data, String key) throws NoSuchAlgorithmException, InvalidKeyException {
byte[] keyBytes = key.getBytes();
SecretKeySpec signingKey = new SecretKeySpec(keyBytes, HMAC_SHA1);
Mac mac = Mac.getInstance(HMAC_SHA1);
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(data.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : rawHmac) {
sb.append(byteToHexString(b));
}
return sb.toString();
}
public static String urlEncode(String url) throws UnsupportedEncodingException {
if (StringUtils.isBlank(url)) {
return null;
}
return URLEncoder.encode(url, "utf-8");
}
private static String byteToHexString(byte ib) {
char[] digit = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};
char[] ob = new char[2];
ob[0] = digit[(ib >>> 4) & 0x0f];
ob[1] = digit[ib & 0x0F];
return new String(ob);
}
}
修改UserController.java
package com.mye.hl21shrio.controller;
import com.mye.hl21shrio.entity.User;
import com.mye.hl21shrio.service.UserService;
import org.apache.shiro.SecurityUtils;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
//省略其他方法
@RequestMapping("register")
public String register(User user){
try {
userService.register(user);
return "redirect:/login.jsp";
} catch (Exception e) {
e.printStackTrace();
}
return "redirect:/register.jsp";
}
}
修改ShiroConfiguration.java
// anon 设置为公共资源,放行要注意anon和authc的顺序
map.put("/user/register","anon");
map.put("/register.jsp","anon");
测试
访问:http://127.0.0.1:8080/register.jsp
查看数据库:
5.MD5、Salt的认证流程上面我们完成了基于MD5+Salt的注册流程,保存到数据库的密码都是经过加密处理的,这时候再用最初的简单密码匹配器进行equals方法进行登录显然是不行的了,我们下面来改造一下认证的流程
修改UserService.java、UserServiceImpl.javaUserService.java
public interface UserService {
//省略其他方法
User findUserByUserName(String userName);
}
UserServiceImpl.java
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
//省略其他方法
@Override
public User findUserByUserName(String userName) {
return userMapper.findUserByUsername(userName);
}
}
修改Usermapper
@Mapper public interface UserMapper extends baseMapper修改CustomerRealm.java及其相关类{ @Select("select * from t_user where username = #{userName}") User findUserByUsername(@Param("userName") String userName); }
CustomerRealm.java
package com.mye.hl21shrio.shiro.realm;
import com.mye.hl21shrio.entity.User;
import com.mye.hl21shrio.service.UserService;
import com.mye.hl21shrio.utils.ApplicationContextUtil;
import com.mye.hl21shrio.utils.CustomerByteSource;
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.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.util.ObjectUtils;
public class CustomerRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 在token中获取用户名
String principal = (String) token.getPrincipal();
// 由于CustomerRealm并没有交由工厂管理,故不能诸如UserService
UserService userService = (UserService) ApplicationContextUtil.getBean("userServiceImpl");
User user = userService.findUserByUserName(principal);
if(!ObjectUtils.isEmpty(user)){
return new SimpleAuthenticationInfo(
user.getUsername(),
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()),
this.getName());
}
return null;
}
}
ApplicationContextUtil.java
package com.mye.hl21shrio.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextUtil implements ApplicationContextAware {
public static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static Object getBean(String beanName){
return context.getBean(beanName);
}
}
修改ShiroConfiguration.java及其相关类
ShiroConfiguration.java
@Configuration
public class ShiroConfiguration {
//省略其他
@Bean
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
//设置密码匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置加密方式
credentialsMatcher.setHashAlgorithmName("MD5");
//设置散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
return customerRealm;
}
}
这里要注意设置的散列数要和生成密码时候的散列数一致
测试 6.实现授权 1.基于角色的授权数据库
# 用户表上面已经有了:t_user DROp TABLE IF EXISTS `t_role`; CREATE TABLE `t_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(64) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `t_user_role`; CREATE TABLE `t_user_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(8) DEFAULT NULL, `role_id` int(8) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
这是t_user表,这里有俩个通过认证的用户
t_role角色表中有两种角色admin和user
我们为用户zhangsan赋予了admin的权限,lisi赋予了user的权限
index.jsp
<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
index
系统主页
退出用户
-
<%-- admin角色的用户能同时拥有用户管理和订单管理的权限,user角色的用户只拥有订单管理的权限 --%>
- 用户管理
- 订单管理
实体类User.java与Role.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_user")
public class User implements Serializable {
@TableField(exist = false)
private List roles = new ArrayList<>();
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_role")
public class Role implements Serializable {
@TableId(type = IdType.AUTO)
private Integer id;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String name;
}
mapper
@Mapper public interface RoleMapper extends baseMapper{ @Select("select r.id,r.name from t_role r left join t_user_role ur on ur.role_id = r.id where ur.user_id = #{userId}") List getRolesByUserId(Integer userId); }
RoleService.java和RoleServiceImpl
public interface RoleService {
List getRolesByUserId(Integer userId);
}
@Service
public class RoleServiceImpl implements RoleService {
@Autowired
private RoleMapper roleMapper;
@Override
public List getRolesByUserId(Integer userId) {
return roleMapper.getRolesByUserId(userId);
}
}
Realm中实现授权
public class CustomerRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取主身份信息
String principal = (String) principalCollection.getPrimaryPrincipal();
// 根据主身份信息获取角色信息
UserService userService = (UserService) ApplicationContextUtil.getBean("userServiceImpl");
User user = userService.findUserByUserName(principal);
RoleService roleService = (RoleService) ApplicationContextUtil.getBean("roleServiceImpl");
List roles = roleService.getRolesByUserId(user.getId());
if(!CollectionUtils.isEmpty(roles)){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
roles.forEach(role -> {
simpleAuthorizationInfo.addRole(role.getName());
});
return simpleAuthorizationInfo;
}
return null;
}
//省略认证
}
测试
上图可以看到admin角色的用户登录系统后能够看到用户管理和订单管理,user角色的用户只能看到订单管理
2.基于权限的授权数据库
DROp TABLE IF EXISTS `t_permission`; CREATE TABLE `t_permission` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(128) DEFAULT NULL, `url` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `t_role_permission`; CREATE TABLE `t_role_permission` ( `id` int(11) NOT NULL AUTO_INCREMENT, `role_id` int(11) DEFAULT NULL, `permission_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
视图层-index.jsp
<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
index
系统主页
退出用户
-
<%-- admin角色的用户能同时拥有用户管理和订单管理的权限,user角色的用户只拥有订单管理的权限 --%>
- 用户管理
-
订单管理
- 新增
- 删除
- 修改
- 查询
实体类Role.java与Permission.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_role")
public class Role implements Serializable {
//其他省略
@TableField(exist = false)
private List permissions = new ArrayList<>();
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_permission")
public class Permission implements Serializable {
@TableId(type = IdType.AUTO)
private Integer id;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String name;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String url;
}
mapper
@Mapper public interface PermissionMapper extends baseMapper{ @Select("select p.id,p.name,p.url from t_permission p left join t_role_permission rp on rp.permission_id = p.id where rp.role_id = #{roleId}") List getPermissionsByRoleId(Integer roleId); }
service层
public interface PermissionService {
List getPermissionsByRoleId(Integer roleId);
}
@Service
public class PermissionServiceImpl implements PermissionService {
@Autowired
private PermissionMapper permissionMapper;
@Override
public List getPermissionsByRoleId(Integer roleId) {
return permissionMapper.getPermissionsByRoleId(roleId);
}
}
Realm中实现授权
public class CustomerRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取主身份信息
String principal = (String) principalCollection.getPrimaryPrincipal();
// 根据主身份信息获取角色信息
UserService userService = (UserService) ApplicationContextUtil.getBean("userServiceImpl");
User user = userService.findUserByUserName(principal);
RoleService roleService = (RoleService) ApplicationContextUtil.getBean("roleServiceImpl");
List roles = roleService.getRolesByUserId(user.getId());
if(!CollectionUtils.isEmpty(roles)){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
roles.forEach(role -> {
simpleAuthorizationInfo.addRole(role.getName());
PermissionService permissionService = (PermissionService) ApplicationContextUtil.getBean("permissionServiceImpl");
List permissions = permissionService.getPermissionsByRoleId(role.getId());
if(!CollectionUtils.isEmpty(permissions)){
permissions.forEach(permission -> {
simpleAuthorizationInfo.addStringPermission(permission.getName());
});
}
});
return simpleAuthorizationInfo;
}
return null;
}
// 省略认证
}
7.EhCache实现缓存测试
shiro提供了缓存管理器,这样在用户第一次认证授权后访问其受限资源的时候就不用每次查询数据库从而达到减轻数据压力的作用,使用shiro的缓存管理器也很简单
修改pom.xmlShiroConfiguration.javaorg.apache.shiro shiro-ehcache 1.4.0
package com.mye.hl21shrio.config;
import com.mye.hl21shrio.global.ShiroConstant;
import com.mye.hl21shrio.shiro.realm.CustomerRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.realm.Realm;
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.HashMap;
import java.util.Map;
@Configuration
public class ShiroConfiguration {
//1.创建shiroFilter //负责拦截所有请求
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//配置系统受限资源
//配置系统公共资源
Map map = new HashMap();
map.put("/user/register","anon");
map.put("/user/login","anon");//anon 设置为公共资源,放行要注意anon和authc的顺序
map.put("/index.jsp","authc");//authc 请求这个资源需要认证和授权
//身份认证失败,则跳转到登录页面的配置 没有登录的用户请求需要登录的页面时自动跳转到登录页面,
// 不是必须的属性,不输入地址的话会自动寻找项目web项目的根目录下的”/login.jsp”页面。
shiroFilterFactoryBean.setLoginUrl("/login.jsp");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//2.创建安全管理器
@Bean(name = "defaultWebSecurityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//给安全管理器设置
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
//3.创建自定义realm
@Bean
public Realm getRealm(@Qualifier("ehCacheManager") EhCacheManager ehCacheManager){
CustomerRealm customerRealm = new CustomerRealm();
//设置密码匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置加密方式
credentialsMatcher.setHashAlgorithmName("MD5");
//设置散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
//设置缓存管理器
customerRealm.setCacheManager(ehCacheManager);
//开启全局缓存
customerRealm.setCachingEnabled(true);
//开启认证缓存并指定缓存名称
customerRealm.setAuthenticationCachingEnabled(true);
customerRealm.setAuthenticationCacheName("authenticationCache");
// 开启授权缓存并指定缓存名称
customerRealm.setAuthorizationCachingEnabled(true);
customerRealm.setAuthorizationCacheName("authorizationCache");
return customerRealm;
}
@Bean(name = "ehCacheManager")
public EhCacheManager ehCacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return cacheManager;
}
}
ehcache.xml
这样就将EhCache集成进来了,但是shiro的这个缓存是本地缓存,也就是说当程序宕机重启后仍然需要从数据库加载数据,不能实现分布式缓存的功能。下面我们来集成redis来实现缓存
8.集成Redis实现Shiro缓存 修改pom.xmlRedisCacheManager.javaorg.springframework.boot spring-boot-starter-data-redis redis.clients jedis 2.9.0
public class RedisCacheManager extends AbstractCacheManager {
private final RedisTemplate redisTemplate;
public RedisCacheManager(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
protected Cache createCache(String cacheName) throws CacheException {
return new RedisCache<>(redisTemplate,cacheName);
}
}
RedisCache.java
import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.springframework.data.redis.core.RedisTemplate; import java.io.Serializable; import java.util.Collection; import java.util.Set; public class RedisCache编写ShiroByteSourceimplements Cache { private String cacheName; private final RedisTemplate redisTemplate; public RedisCache(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } public RedisCache(RedisTemplate redisTemplate, String name) { this(redisTemplate); this.cacheName = name; } @Override public V get(K k) throws CacheException { return (V) redisTemplate.opsForHash().get(this.cacheName,k.toString()); } @Override public V put(K k, V v) throws CacheException { redisTemplate.opsForHash().put(this.cacheName,k.toString(),v); return null; } @Override public V remove(K k) throws CacheException { return (V) redisTemplate.opsForHash().delete(this.cacheName,k.toString()); } @Override public void clear() throws CacheException { redisTemplate.delete(this.cacheName); } @Override public int size() { return redisTemplate.opsForHash().size(this.cacheName).intValue(); } @Override public Set keys() { return (Set ) redisTemplate.opsForHash().keys(this.cacheName); } @Override public Collection values() { return (Collection ) redisTemplate.opsForHash().values(this.cacheName); } }
package com.mye.hl21shrio.shiro.cache;
import org.apache.shiro.codec.base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.codec.Hex;
import org.apache.shiro.util.ByteSource;
import java.io.Serializable;
import java.util.Arrays;
public class ShiroByteSource implements ByteSource, Serializable {
private static final long serialVersionUID = -6814382603612799610L;
private volatile byte[] bytes;
private String cachedHex;
private String cachedbase64;
public ShiroByteSource() {
}
public ShiroByteSource(String string) {
this.bytes = CodecSupport.toBytes(string);
}
public void setBytes(byte[] bytes) {
this.bytes = bytes;
}
@Override
public byte[] getBytes() {
return this.bytes;
}
@Override
public String toHex() {
if ( this.cachedHex == null ) {
this.cachedHex = Hex.encodeToString(getBytes());
}
return this.cachedHex;
}
@Override
public String tobase64() {
if ( this.cachedbase64 == null ) {
this.cachedbase64 = base64.encodeToString(getBytes());
}
return this.cachedbase64;
}
@Override
public boolean isEmpty() {
return this.bytes == null || this.bytes.length == 0;
}
@Override
public String toString() {
return tobase64();
}
@Override
public int hashCode() {
if (this.bytes == null || this.bytes.length == 0) {
return 0;
}
return Arrays.hashCode(this.bytes);
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o instanceof ByteSource) {
ByteSource bs = (ByteSource) o;
return Arrays.equals(getBytes(), bs.getBytes());
}
return false;
}
public static ByteSource of(String string) {
return new ShiroByteSource(string);
}
}
然后修改CustomerRealm的认证方法,不然会提示序列化失败,因为源码中的ByteSource没有序列化
修改ShiroConfiguration.javapackage com.mye.hl21shrio.config;
import com.mye.hl21shrio.shiro.cache.RedisCacheManager;
import com.mye.hl21shrio.shiro.realm.CustomerRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.Session;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class ShiroConfiguration {
//1.创建shiroFilter //负责拦截所有请求
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//配置系统受限资源
//配置系统公共资源
Map map = new HashMap();
map.put("/user/register","anon");
map.put("/user/login","anon");//anon 设置为公共资源,放行要注意anon和authc的顺序
map.put("/index.jsp","authc");//authc 请求这个资源需要认证和授权
//身份认证失败,则跳转到登录页面的配置 没有登录的用户请求需要登录的页面时自动跳转到登录页面,
// 不是必须的属性,不输入地址的话会自动寻找项目web项目的根目录下的”/login.jsp”页面。
shiroFilterFactoryBean.setLoginUrl("/login.jsp");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//2.创建安全管理器
@Bean(name = "defaultWebSecurityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//给安全管理器设置
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
//3.创建自定义realm
@Bean
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
//设置密码匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置加密方式
credentialsMatcher.setHashAlgorithmName("MD5");
//设置散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
//设置缓存管理器
if (connectionFactory()!= null){
customerRealm.setCacheManager(redisCacheManager());
}else {
customerRealm.setCacheManager(ehCacheManager());
}
//开启全局缓存
customerRealm.setCachingEnabled(true);
//开启认证缓存并指定缓存名称
customerRealm.setAuthenticationCachingEnabled(true);
customerRealm.setAuthenticationCacheName("authenticationCache");
// 开启授权缓存并指定缓存名称
customerRealm.setAuthorizationCachingEnabled(true);
customerRealm.setAuthorizationCacheName("authorizationCache");
return customerRealm;
}
@Bean(name = "ehCacheManager")
public EhCacheManager ehCacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return cacheManager;
}
@Bean("connectionFactory")
public JedisConnectionFactory connectionFactory() {
String redisHost = "127.0.0.1";
int redisPort = 6379;
//redis配置单节点
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(redisHost,redisPort);
return new JedisConnectionFactory(redisStandaloneConfiguration);
}
@Bean(name = "redisTemplate")
public RedisTemplate redisTemplate(){
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory());
return redisTemplate;
}
@Bean(name = "redisCacheManager")
@DependsOn(value = "redisTemplate")
public RedisCacheManager redisCacheManager(){
return new RedisCacheManager(redisTemplate());
}
}
测试
上图可以看到我们用户登录以后用户的认证和授权数据已经缓存到redis了,这个时候即使程序重启,redis中的缓存数据也不会删除,除非用户自己退出登录。
9.集成图片验证码 login.jsp<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
login
用户登录
工具类-VerifyCodeUtil.java
package com.mye.hl21shrio.utils;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Random;
public class VerifyCodeUtil {
//使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
private static Random random = new Random();
public static String generateVerifyCode(int verifySize) {
return generateVerifyCode(verifySize, VERIFY_CODES);
}
public static String generateVerifyCode(int verifySize, String sources) {
if (sources == null || sources.length() == 0) {
sources = VERIFY_CODES;
}
int codesLen = sources.length();
Random rand = new Random(System.currentTimeMillis());
StringBuilder verifyCode = new StringBuilder(verifySize);
for (int i = 0; i < verifySize; i++) {
verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));
}
return verifyCode.toString();
}
public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException {
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, outputFile, verifyCode);
return verifyCode;
}
public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException {
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, os, verifyCode);
return verifyCode;
}
public static void outputImage(int w, int h, File outputFile, String code) throws IOException {
if (outputFile == null) {
return;
}
File dir = outputFile.getParentFile();
if (!dir.exists()) {
dir.mkdirs();
}
try {
outputFile.createNewFile();
FileOutputStream fos = new FileOutputStream(outputFile);
outputImage(w, h, fos, code);
fos.close();
} catch (IOException e) {
throw e;
}
}
public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
int verifySize = code.length();
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Random rand = new Random();
Graphics2D g2 = image.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Color[] colors = new Color[5];
Color[] colorSpaces = new Color[]{Color.WHITE, Color.CYAN, Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE, Color.PINK, Color.YELLOW};
float[] fractions = new float[colors.length];
for (int i = 0; i < colors.length; i++) {
colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
fractions[i] = rand.nextFloat();
}
Arrays.sort(fractions);
g2.setColor(Color.GRAY);// 设置边框色
g2.fillRect(0, 0, w, h);
Color c = getRandColor(200, 250);
g2.setColor(c);// 设置背景色
g2.fillRect(0, 2, w, h - 4);
//绘制干扰线
Random random = new Random();
g2.setColor(getRandColor(160, 200));// 设置线条的颜色
for (int i = 0; i < 20; i++) {
int x = random.nextInt(w - 1);
int y = random.nextInt(h - 1);
int xl = random.nextInt(6) + 1;
int yl = random.nextInt(12) + 1;
g2.drawLine(x, y, x + xl + 40, y + yl + 20);
}
// 添加噪点
float yawpRate = 0.05f;// 噪声率
int area = (int) (yawpRate * w * h);
for (int i = 0; i < area; i++) {
int x = random.nextInt(w);
int y = random.nextInt(h);
int rgb = getRandomIntColor();
image.setRGB(x, y, rgb);
}
shear(g2, w, h, c);// 使图片扭曲
g2.setColor(getRandColor(100, 160));
int fontSize = h - 4;
Font font = new Font("Algerian", Font.ITALIC, fontSize);
g2.setFont(font);
char[] chars = code.toCharArray();
for (int i = 0; i < verifySize; i++) {
AffineTransform affine = new AffineTransform();
affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2);
g2.setTransform(affine);
g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
}
g2.dispose();
ImageIO.write(image, "jpg", os);
}
private static Color getRandColor(int fc, int bc) {
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
private static int getRandomIntColor() {
int[] rgb = getRandomRgb();
int color = 0;
for (int c : rgb) {
color = color << 8;
color = color | c;
}
return color;
}
private static int[] getRandomRgb() {
int[] rgb = new int[3];
for (int i = 0; i < 3; i++) {
rgb[i] = random.nextInt(255);
}
return rgb;
}
private static void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
}
private static void shearX(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(2);
boolean borderGap = true;
int frames = 1;
int phase = random.nextInt(2);
for (int i = 0; i < h1; i++) {
double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
g.copyArea(0, i, w1, 1, (int) d, 0);
if (borderGap) {
g.setColor(color);
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + w1, i, w1, i);
}
}
}
private static void shearY(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(40) + 10; // 50;
boolean borderGap = true;
int frames = 20;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
g.copyArea(i, 0, 1, h1, 0, (int) d);
if (borderGap) {
g.setColor(color);
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
}
}
}
public static void main(String[] args) throws IOException {
//获取验证码
String s = generateVerifyCode(4);
//将验证码放入图片中
outputImage(260, 60, new File(""), s);
System.out.println(s);
}
}
Controller层
UserController.java中有两处变动,其一是需要一个生成验证码的方法并输出到页面;其二是要修改认证的流程,先进行验证码的校验,验证码校验通过以后才可以进行Shiro的认证
package com.mye.hl21shrio.controller;
import com.mye.hl21shrio.entity.User;
import com.mye.hl21shrio.service.UserService;
import com.mye.hl21shrio.utils.VerifyCodeUtil;
import org.apache.shiro.SecurityUtils;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public String login(String username,String password, String verifyCode,HttpSession session){
// 校验验证码
String verifyCodes = (String) session.getAttribute("verifyCode");
// 获取当前登录用户
Subject subject = SecurityUtils.getSubject();
try {
if(verifyCodes.equalsIgnoreCase(verifyCode)){
subject.login(new UsernamePasswordToken(username,password));
return "redirect:/index.jsp";
} else {
throw new RuntimeException("验证码错误");
}
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误~");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误~");
} catch (Exception e) {
e.printStackTrace();
}
return "redirect:/login.jsp";
}
@RequestMapping("getImage")
public void getImage(HttpSession session, HttpServletResponse response) throws IOException {
//生成验证码
String verifyCode = VerifyCodeUtil.generateVerifyCode(4);
//验证码放入session
session.setAttribute("verifyCode",verifyCode);
//验证码存入图片
ServletOutputStream os = response.getOutputStream();
response.setContentType("image/png");
VerifyCodeUtil.outputImage(180,40,os,verifyCode);
}
}
修改ShiroConfiguration.java
map.put("/user/getImage","anon");



