此demo用SpringBoot+Shiro简单实现了登陆、注册、认证、授权的功能,并用redis做分布式缓存提高性能。
导入pom.xml
<?xml version="1.0" encoding="UTF-8"?> <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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.2.RELEASE</version> </parent> <groupId>com.ego</groupId> <artifactId>shirodemo</artifactId> <version>1.0-SNAPSHOT</version> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpcore</artifactId> <version>4.4.13</version> </dependency> <!-- 引入jsp依赖 --> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-starter</artifactId> <version>1.5.3</version> </dependency> <!-- mybatis plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.1</version> </dependency> <!-- Druid数据源 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <!-- Mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <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> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.5.3</version> </dependency> <!-- 引入redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies> </project>
配置yml文件
spring: # 设置视图模板为jsp mvc: view: prefix: / suffix: .jsp datasource: type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/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 redis: host: 127.0.0.1 port: 6379 password: abc123456 database: 0 mybatis-plus: type-aliases-package: com.ego.pojo configuration: map-underscore-to-camel-case: true
<%--解决页面乱码--%> <%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %> <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>index</title> </head> <body> <form action="${pageContext.request.contextPath}/user/login" method="post"> 用户名:<input type="text" name="username" > <br/> 密码 : <input type="text" name="password"> <br> <input type="submit" value="登录"> </form> </body> </html> <%--解决页面乱码--%> <%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %> <%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>index</title> </head> <body> <h1>系统主页</h1> <a href="${pageContext.request.contextPath}/user/logout">退出用户</a> <ul> <%-- admin角色的用户能同时拥有用户管理和订单管理的权限,user角色的用户只拥有订单管理的权限 --%> <shiro:hasRole name="admin"> <li> <a href="">用户管理</a> </li> </shiro:hasRole> <%-- admin角色的用户对订单有增删改查的权限,user角色的用户只能查看订单 --%> <shiro:hasAnyRoles name="admin,user"> <li> <a href="">订单管理</a> <ul> <shiro:hasPermission name="order:add:*"> <li><a href="">新增</a></li> </shiro:hasPermission> <shiro:hasPermission name="order:del:*"> <li><a href="">删除</a></li> </shiro:hasPermission> <shiro:hasPermission name="order:upd:*"> <li><a href="">修改</a></li> </shiro:hasPermission> <shiro:hasPermission name="order:find:*"> <li><a href="">查询</a></li> </shiro:hasPermission> </ul> </li> </shiro:hasAnyRoles> </ul> </body> </html> <%--解决页面乱码--%> <%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %> <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>register</title> </head> <body> <h1>用户注册</h1> <form action="${pageContext.request.contextPath}/user/register" method="post"> 用户名:<input type="text" name="username" > <br/> 密码 : <input type="text" name="password"> <br> <input type="submit" value="立即注册"> </form> </body> </html>
package com.ego.pojo; import com.baomidou.mybatisplus.annotation.*; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.ArrayList; import java.util.List; /** * @author 袁梦达 2019012364 */ @Data @NoArgsConstructor @AllArgsConstructor @TableName("t_user") @ApiModel("用户实体类") public class User implements Serializable { /** 数据库中设置该字段自增时该注解不能少 **/ @TableId(type = IdType.AUTO) @ApiModelProperty(name = "id", value = "ID 主键") private Integer id; @TableField(fill = FieldFill.INSERT_UPDATE) @ApiModelProperty(name = "username", value = "用户名") private String username; @TableField(fill = FieldFill.INSERT_UPDATE) @ApiModelProperty(name = "password", value = "密码") private String password; @TableField(fill = FieldFill.INSERT) @ApiModelProperty(name = "salt", value = "盐") private String salt; @TableField(fill = FieldFill.INSERT_UPDATE) @ApiModelProperty(name = "age", value = "年龄") private Integer age; @TableField(fill = FieldFill.INSERT_UPDATE) @ApiModelProperty(name = "email", value = "邮箱") private String email; @TableField(fill = FieldFill.INSERT_UPDATE) @ApiModelProperty(name = "address", value = "地址") private String address; @TableField(exist = false) private List<Role> roles = new ArrayList<>(); } package com.ego.pojo; import com.baomidou.mybatisplus.annotation.*; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.ArrayList; import java.util.List; /** * @author 袁梦达 2019012364 */ @Data @NoArgsConstructor @AllArgsConstructor @TableName("t_role") @ApiModel("角色实体类") public class Role implements Serializable { /** 数据库中设置该字段自增时该注解不能少 **/ @TableId(type = IdType.AUTO) @ApiModelProperty(name = "id", value = "ID 主键") private Integer id; @TableField(fill = FieldFill.INSERT_UPDATE) @ApiModelProperty(name = "name", value = "角色名称") private String name; @TableField(exist = false) private List<Permission> permissions = new ArrayList<>(); } package com.ego.pojo; import com.baomidou.mybatisplus.annotation.*; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; /** * @author 袁梦达 2019012364 */ @Data @NoArgsConstructor @AllArgsConstructor @TableName("t_permission") @ApiModel("权限实体类") public class Permission implements Serializable { /** 数据库中设置该字段自增时该注解不能少 **/ @TableId(type = IdType.AUTO) @ApiModelProperty(name = "id", value = "ID主键") private Integer id; @TableField(fill = FieldFill.INSERT_UPDATE) @ApiModelProperty(name = "name", value = "权限名称") private String name; @TableField(fill = FieldFill.INSERT_UPDATE) @ApiModelProperty(name = "url", value = "权限菜单URL") private String url; }
这里dao采用了mybatis-plus
package com.ego.controller; import com.ego.pojo.User; import com.ego.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; /** * @author 袁梦达 2019012364 */ @Controller @RequestMapping("/user") public class UserController { @Autowired private UserService userService; /** * 用户登录 * @param username * @param password * @return */ @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) { System.out.println("密码错误!"); } catch (Exception e) { } // 如果认证失败仍然回到登录页面 return "redirect:/login.jsp"; } @RequestMapping("/logout") public String logout(){ subject.logout(); // 退出后仍然会到登录页面 * 用户注册 * @param user @RequestMapping("/register") public String register(User user){ userService.register(user); return "redirect:/login.jsp"; return "redirect:/register.jsp"; } package com.ego.service.impl; import com.ego.dao.mapper.UserMapper; import com.ego.shiro.ShiroConstant; import com.ego.utils.SaltUtil; import org.apache.shiro.crypto.hash.Md5Hash; import org.springframework.stereotype.Service; @Service("userService") public class UserServiceImpl implements UserService { private UserMapper userMapper; @Override public void register(User user) { //生成随机盐 String salt = SaltUtil.getSalt(ShiroConstant.SALT_LENGTH); //保存随机盐 user.setSalt(salt); //生成密码 Md5Hash password = new Md5Hash(user.getPassword(), salt, ShiroConstant.HASH_ITERATORS); //保存密码 user.setPassword(password.toHex()); userMapper.insert(user); public User findUserByUserName(String userName) { return userMapper.findUserByUserName(userName); import com.ego.dao.mapper.RoleMapper; import com.ego.pojo.Role; import com.ego.service.RoleService; import java.util.List; @Service("roleService") public class RoleServiceImpl implements RoleService { private RoleMapper roleMapper; public List<Role> getRolesByUserId(Integer userId) { return roleMapper.getRolesByUserId(userId); import com.ego.dao.mapper.PermissionMapper; import com.ego.pojo.Permission; import com.ego.service.PermissionService; @Service("permissionService") public class PermissionServiceImpl implements PermissionService { private PermissionMapper permissionMapper; public List<Permission> getPermissionsByRoleId(Integer roleId) { return permissionMapper.getPermissionsByRoleId(roleId); package com.ego.dao.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; @Mapper public interface UserMapper extends BaseMapper<User> { @Select("SELECT u.id,u.username,u.password,u.salt,u.age,u.email,u.address FROM t_user u WHERE u.username = #{username}") User findUserByUserName(String username); public interface RoleMapper extends BaseMapper<Role> { @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<Role> getRolesByUserId(Integer userId); public interface PermissionMapper extends BaseMapper<Permission> { @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<Permission> getPermissionsByRoleId(Integer roleId);
package com.ego.utils; import java.util.Random; /** * @author 袁梦达 2019012364 */ public class SaltUtil { public static String getSalt(int n){ char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890!@#$%^&*()".toCharArray(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; i++) { char aChar = chars[new Random().nextInt(chars.length)]; sb.append(aChar); } return sb.toString(); } } 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 { this.context = applicationContext; /** * 根据工厂中的类名获取类实例 */ public static Object getBean(String beanName){ return context.getBean(beanName);
package com.ego.utils; import java.util.Random; /** * @author 袁梦达 2019012364 */ public class SaltUtil { public static String getSalt(int n){ char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890!@#$%^&*()".toCharArray(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; i++) { char aChar = chars[new Random().nextInt(chars.length)]; sb.append(aChar); } return sb.toString(); } } package com.ego.utils; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; /** * @author 袁梦达 2019012364 */ @Component public class ApplicationContextUtil implements ApplicationContextAware { public static ApplicationContext context; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context = applicationContext; } /** * 根据工厂中的类名获取类实例 */ public static Object getBean(String beanName){ return context.getBean(beanName); } }
package com.ego.shiro; import com.ego.shiro.cache.RedisCacheManager; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; 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; /** * @author 袁梦达 2019012364 */ @Configuration public class ShiroConfiguration { //1.创建shiroFilter //负责拦截所有请求 @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //给filter设置安全管理器 shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager); //配置系统受限资源 //配置系统公共资源 Map<String,String> map = new HashMap<String,String>(); map.put("/user/login", "anon"); map.put("/user/register","anon"); map.put("/register.jsp","anon"); map.put("/index.jsp","authc");//authc 请求这个资源需要认证和授权 //默认认证界面路径 shiroFilterFactoryBean.setLoginUrl("/login.jsp"); shiroFilterFactoryBean.setFilterChainDefinitionMap(map); return shiroFilterFactoryBean; } //2.创建安全管理器 public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){ DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); //给安全管理器设置 defaultWebSecurityManager.setRealm(realm); return defaultWebSecurityManager; //3.创建自定义realm public Realm getRealm(){ CustomerRealm customerRealm = new CustomerRealm(); // 设置密码匹配器 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); // 设置加密方式 credentialsMatcher.setHashAlgorithmName(ShiroConstant.HASH_ALGORITHM_NAME.MD5); // 设置散列次数 credentialsMatcher.setHashIterations(ShiroConstant.HASH_ITERATORS); customerRealm.setCredentialsMatcher(credentialsMatcher); // 设置缓存管理器 customerRealm.setCacheManager(new RedisCacheManager()); // 开启全局缓存 customerRealm.setCachingEnabled(true); // 开启认证缓存并指定缓存名称 customerRealm.setAuthenticationCachingEnabled(true); customerRealm.setAuthenticationCacheName("authenticationCache"); // 开启授权缓存并指定缓存名称 customerRealm.setAuthorizationCachingEnabled(true); customerRealm.setAuthorizationCacheName("authorizationCache"); return customerRealm; } public class ShiroConstant { /** 随机盐的位数 **/ public static final int SALT_LENGTH = 8; /** hash的散列次数 **/ public static final int HASH_ITERATORS = 1024; public interface HASH_ALGORITHM_NAME { String MD5 = "MD5"; import com.ego.pojo.Permission; import com.ego.pojo.Role; import com.ego.pojo.User; import com.ego.service.PermissionService; import com.ego.service.RoleService; import com.ego.service.UserService; import com.ego.utils.ApplicationContextUtil; 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.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import java.util.List; public class CustomerRealm extends AuthorizingRealm { //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // 获取主身份信息 String principal = (String) principals.getPrimaryPrincipal(); // 根据主身份信息获取角色信息 UserService userService = (UserService) ApplicationContextUtil.getBean("userService"); User user = userService.findUserByUserName(principal); RoleService roleService = (RoleService) ApplicationContextUtil.getBean("roleService"); List<Role> 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("permissionService"); List<Permission> permissions = permissionService.getPermissionsByRoleId(role.getId()); if(!CollectionUtils.isEmpty(permissions)){ permissions.forEach(permission -> { simpleAuthorizationInfo.addStringPermission(permission.getName()); }); } }); return simpleAuthorizationInfo; } return null; //认证 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String principal = (String) token.getPrincipal(); if(!ObjectUtils.isEmpty(user)){ return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(), new CustomerByteSource(user.getSalt()),this.getName()); 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.File; import java.io.InputStream; import java.io.Serializable; import java.util.Arrays; //自定义salt实现 实现序列化接口 public class CustomerByteSource implements ByteSource, Serializable { private byte[] bytes; private String cachedHex; private String cachedBase64; public CustomerByteSource() { public CustomerByteSource(byte[] bytes) { this.bytes = bytes; public CustomerByteSource(char[] chars) { this.bytes = CodecSupport.toBytes(chars); public CustomerByteSource(String string) { this.bytes = CodecSupport.toBytes(string); public CustomerByteSource(ByteSource source) { this.bytes = source.getBytes(); public CustomerByteSource(File file) { this.bytes = (new CustomerByteSource.BytesHelper()).getBytes(file); public CustomerByteSource(InputStream stream) { this.bytes = (new CustomerByteSource.BytesHelper()).getBytes(stream); public static boolean isCompatible(Object o) { return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream; public byte[] getBytes() { return this.bytes; public boolean isEmpty() { return this.bytes == null || this.bytes.length == 0; public String toHex() { if (this.cachedHex == null) { this.cachedHex = Hex.encodeToString(this.getBytes()); return this.cachedHex; public String toBase64() { if (this.cachedBase64 == null) { this.cachedBase64 = Base64.encodeToString(this.getBytes()); return this.cachedBase64; public String toString() { return this.toBase64(); public int hashCode() { return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0; public boolean equals(Object o) { if (o == this) { return true; } else if (o instanceof ByteSource) { ByteSource bs = (ByteSource) o; return Arrays.equals(this.getBytes(), bs.getBytes()); } else { return false; private static final class BytesHelper extends CodecSupport { private BytesHelper() { public byte[] getBytes(File file) { return this.toBytes(file); public byte[] getBytes(InputStream stream) { return this.toBytes(stream);