做管理系统权限这块很重要,一般会通过角色来划分权限,更复杂的还可以做成数据权限,大部分的场景都是基于角色来实现的,最近项目的需求是实现后台管理系统权限控制,不同的角色有不同的权限,基于角色来实现。
认证原理
- 用户登陆之后,使用密码对账号进行签名生成并返回token并设置过期时间;
- 将token保存到本地,并且每次发送请求时都在header上携带token。
- shiro过滤器拦截到请求并获取header中的token,并提交到自定义realm的doGetAuthenticationInfo方法。
- 通过jwt解码获取token中的用户名,从数据库中查询到密码之后根据密码生成jwt效验器并对token进行验证。
表结构
首先看一下用到的表结构。
系统用户表:sys_user
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| CREATE TABLE `sys_user` ( `id` int(11) NOT NULL AUTO_INCREMENT , `login_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录名' , `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码' , `real_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '真实名字' , `last_login_time` datetime NULL DEFAULT NULL COMMENT '最后登录时间' , `last_login_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录IP' , `status` tinyint(1) NULL DEFAULT NULL COMMENT '用户状态:0,正常 1,冻结' , `email` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱' , `mobile` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '联系电话' , `role_ids` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '角色id' , `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间' , `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间' , `create_by` int(11) NULL DEFAULT NULL COMMENT '更新人' , `update_by` int(11) NULL DEFAULT NULL COMMENT '修改人' , `salt` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '盐值' , `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '删除标识:0,正常 1,删除' , PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='系统用户表' AUTO_INCREMENT=2 ROW_FORMAT=DYNAMIC ;
|
系统角色表:sys_role
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| CREATE TABLE `sys_role` ( `id` int(11) NOT NULL AUTO_INCREMENT , `role_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '角色名称' , `description` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述' , `menu_ids` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '菜单id' , `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间' , `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间' , `create_by` int(11) NULL DEFAULT NULL COMMENT '创建人' , `update_by` int(11) NULL DEFAULT NULL COMMENT '更新人' , `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '删除标识:0,正常 1,删除' , PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='系统角色表' AUTO_INCREMENT=2 ROW_FORMAT=DYNAMIC ;
|
系统菜单表:sys_menu
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| CREATE TABLE `sys_menu` ( `id` int(11) NOT NULL AUTO_INCREMENT , `pid` int(11) NULL DEFAULT NULL COMMENT '菜单父id' , `menu_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '菜单名称' , `menu_url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '菜单url' , `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间' , `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间' , `create_by` int(11) NULL DEFAULT NULL COMMENT '创建人' , `update_by` int(11) NULL DEFAULT NULL COMMENT '更新人' , `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '删除标识:0,正常 1,删除' , PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='系统菜单表' AUTO_INCREMENT=8 ROW_FORMAT=DYNAMIC ;
|
系统权限表:sys_btn
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| CREATE TABLE `sys_btn` ( `id` int(11) NOT NULL AUTO_INCREMENT , `btn_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限名称' , `btn_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限编码' , `btn_url` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限路径' , `menu_id` int(11) NULL DEFAULT NULL COMMENT '菜单id' , PRIMARY KEY (`id`), INDEX `index_menu_id` (`menu_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='系统权限表' AUTO_INCREMENT=35 ROW_FORMAT=DYNAMIC ;
|
系统授权表:sys_author
1 2 3 4 5 6 7 8 9 10
| CREATE TABLE `sys_author` ( `role_id` int(11) NULL DEFAULT NULL COMMENT '角色id' , `res_id` int(11) NULL DEFAULT NULL COMMENT '资源id' , `res_type` tinyint(1) NULL DEFAULT NULL COMMENT '资源类型' ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='系统授权表' ROW_FORMAT=DYNAMIC ;
|
这是本次涉及到的表结构,表结构设计好了,相关业务也基本清楚了,下面直接集成springboot
和shiro
,实现权限控制。
引入JAR
1 2 3 4 5 6 7 8 9 10 11 12
| <!--JWT--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency> <!--shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency>
|
自定义JwtToken
首先我们需要自定义一个对象用来封装oken。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import org.apache.shiro.authc.AuthenticationToken;
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String token) { this.token = token; }
@Override public Object getPrincipal() { return token; }
@Override public Object getCredentials() { return token; } }
|
JwtUtil
还得一个工具类用来进行签名和效验Token。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;
public class JwtUtil {
private static final long EXPIRE_TIME = 5 * 60 * 1000;
public static boolean verify(String token, String username, String secret) { try { Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withClaim("username", username) .build(); DecodedJWT jwt = verifier.verify(token); return true; } catch (IllegalArgumentException e) { e.printStackTrace(); return false; } catch (JWTVerificationException e) { e.printStackTrace(); return false; } }
public static String getUsername(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); } catch (JWTDecodeException e) { e.printStackTrace(); return null; } }
public static String sign(String username, String secret) { Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(secret); return JWT.create() .withClaim("username", username) .withExpiresAt(date) .sign(algorithm); } }
|
ShiroFilter拦截器
在这里我们要使用shiro来拦截token,需要我们自己写一个jwt的过滤器来作为shiro的过滤器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
public class JwtFilter extends BasicHttpAuthenticationFilter {
@Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { try { executeLogin(request, response); return true; } catch (Exception e) { e.printStackTrace(); return false; } }
@Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader("Authorization"); JwtToken jwtToken = new JwtToken(token); getSubject(request, response).login(jwtToken); return true; } }
|
自定义Realm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| import club.itwork.backend.api.entity.SysUser; import club.itwork.backend.api.service.SysUserService; import club.itwork.common.util.StringUtil; 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.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component;
import java.util.ArrayList; import java.util.List;
@Component public class ShiroRealm extends AuthorizingRealm {
@Autowired private SysUserService sysUserService;
@Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { String token = (String) auth.getPrincipal(); String username = JwtUtil.getUsername(token); if (StringUtil.isEmpty(username)) { throw new IncorrectCredentialsException("用户名无效"); }
SysUser user = sysUserService.getByUsername(username); if (user == null) { throw new UnknownAccountException("用户不存在"); }
if(!JwtUtil.verify(token,username,user.getPassword())){ throw new AuthenticationException("密码错误"); }
return new SimpleAuthenticationInfo(token, token, getName()); }
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = JwtUtil.getUsername(principals.toString()); SysUser user = sysUserService.getByUsername(username); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); List roles = new ArrayList(); roles.parallelStream().forEach(role ->{ info.addStringPermission(""); }); return info; } }
|
ShiroConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| import club.itwork.backend.shiro.JwtFilter; import club.itwork.backend.shiro.ShiroRealm; import org.apache.shiro.mgt.DefaultSessionStorageEvaluator; import org.apache.shiro.mgt.DefaultSubjectDAO; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map;
@Configuration public class ShiroConfig {
@Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/login/**", "anon"); filterChainDefinitionMap.put("/**.js", "anon"); filterChainDefinitionMap.put("/swagger**/**", "anon"); filterChainDefinitionMap.put("/webjars/**", "anon"); Map<String, Filter> filterMap = new HashMap<>(1); filterMap.put("jwt", new JwtFilter()); factoryBean.setFilters(filterMap); filterChainDefinitionMap.put("/**", "jwt"); factoryBean.setUnauthorizedUrl("/403"); factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return factoryBean; }
@Bean("securityManager") public SecurityManager securityManager(ShiroRealm realm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator(); evaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(evaluator); securityManager.setSubjectDAO(subjectDAO); return securityManager; }
@Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; }
@Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); }
@Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
|
登录Demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.subject.Subject; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
@RestController public class LoginController {
@PostMapping("/login") public RespData login(String username, String password, String code) { try { Subject subject = SecurityUtils.getSubject(); String token = JwtUtil.sign(username, MD5Util.encode(password, "UTF-8", false)); JwtToken jwtToken = new JwtToken(token); subject.login(jwtToken); return RespData.successMsg("登录成功"); } catch (UnknownAccountException ex) { return RespData.errorMsg("用户不存在!"); } catch (IncorrectCredentialsException ex) { return RespData.errorMsg("用户名无效!"); } catch (AuthenticationException ae) { return RespData.errorMsg("密码错误!"); } }
@RequestMapping(value = "/validateCode") public String validateCode(HttpServletResponse response) { response.setContentType("image/jpeg"); response.setHeader("Pragma", "no-cache"); response.setHeader("Cache-Control", "no-cache"); response.setDateHeader("Expires", 0);
ValidateCodeUtil code = new ValidateCodeUtil(110, 34, 4, 50); try { code.write(response.getOutputStream()); } catch (Exception e) { e.printStackTrace(); } return null; } }
|
这是我本次项目中springboot
集成shiro
的过程,具体配置还得根据自己的项目结构集成。
使用JWT的好处
- 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。
- 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库。
- 安全(security): 与简单的JSON相比,XML和XML数字签名会引入复杂的安全漏洞。
参考:springboot2+shiro+jwt整合
获取源码
关注公众号「特想学英语」,回复 shiro-jwt