Springboot+Shiro+Jwt实现权限控制的项目实践
作者:搬山道猿
前置背景
为什么写下这篇文章?
因为需要实现一个设备管理系统的权限管理模块,在查阅很多博客以及其他网上资料之后,发现重复、无用的博客很多,因此写一篇文章来记录,以便后面复习。
涉及的知识点主要有下列知识点:
- JWT
- shiro
书写顺序
- 首先使用springboot 结合 jwt完成前后端分离的token认证。
- 其次结合shiro完成shiro+jwt的前后端分离的权限认证管理。
权限管理的表结构设计
一个user可以拥有多个role,一个role也可以被多个user拥有, 一个 角色拥有多个权限即功能,一个权限可以被多个role拥有。
用户、角色、权限类
表结构图
Part1: spring boot + jwt
这一部分就可以完成前后端分离项目的登录功能。在不需要添加权限管理的情况下,就可以满足需求。
Spring boot集成JWT
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.8.2</version> </dependency>
思路整理
- 为什么需要使用到jwt?
- jwt是什么
- Java如何使用jwt
在前后端分离的项目中,由服务器使用的会话管理 session
无法满足需求。需要一种技术做会话管理。因此选择 JWT
。 Json web token (JWT)
: 是目前流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。 JWT
的数据结构分为三部分 header payload signature。 这三部分通过 .
连接,如下
Token示例:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJwYXNzd29yZCI6InN1cGVyIiwiZXhwIjoxNjYzMTIzNDgzLCJ1c2VybmFtZSI6InN1cGVyIn0.
5xVg6IuOLe_uVwwOaeyRDbTHRjfmIbNsnb-DP9-Ic20
如何使用:当用户登录系统后,服务端给前端发送一个基于用户信息创建的token,然后在此后的每一次前端请求都会携带token。服务端通过拦截器拦截请求,同时验证携带的token是否正确。如果正确则放行请求,不正确则拒绝通过。 思路流程图:
token的创建和验证
JWTUtil.java
负责创建和验证jwt格式的token
public class JWTUtil { private static final long EXPIRE_TIME = 3 * 60 * 1000;//默认3分钟 //私钥 private static final String TOKEN_SECRET = "privateKey"; public static String createToken(UserEntity userModel) { try { // 设置过期时间 Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); log.info(String.valueOf(date)); // 私钥和加密算法 Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); // 设置头部信息 Map<String, Object> header = new HashMap<>(2); header.put("Type", "Jwt"); header.put("alg", "HSA256"); // 返回token字符串 return JWT.create() .withHeader(header) .withClaim("username", userModel.getUsername()) .withExpiresAt(date) .sign(algorithm); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 检验token是否正确 * * @param **token** * @return */ public static boolean verifyToken(String token, String username) { log.info("验证token.."); try { Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); JWTVerifier verifier = JWT.require(algorithm) .withClaim("username",username).build(); // 验证不通过会抛出异常。 verifier.verify(token); return true; } catch (Exception e) { log.info("verifyToken = {}",e.getMessage()); return false; } } // 通过withClaim添加在token里面的数据都可以通过这种方式获取 public static String getUsername(String token){ DecodedJWT jwt = JWT.decode(token); String username = String.valueOf(jwt.getClaim("username")); if (StringUtils.hasLength(username)){ return username; } return null; } }
拦截器的创建和配置
创建拦截器,拦截请求
@Slf4j @Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 当前端是通过在请求里面以 token="xxxx.xxx.zzz"的方式传递时,通过getHeader("token") // 的方式获取。 String token = request.getHeader("token"); log.info("token = {}",token); if (token == null){ setReturnInfo((HttpServletResponse) response,401,"请携带token"); return false; } // 解析token中的数据,JWTUtil.getUsername(); // 在这里可以通过findUserByUsername的方式从数据源中获取数据 // 假定登录用户是super, 并传递给此方法传递参数 if ( !JWTUtil.verifyToken(token,"super")){ setReturnInfo((HttpServletResponse) response,401,"token已过期"); return false; } return true; } private static void setReturnInfo(HttpServletResponse httpResponse,int status,String msg) throws IOException { log.info("token = null"); httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpResponse.setHeader("Access-Control-Allow-Origin", "*"); httpResponse.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=utf-8"); Map<String,String> result =new HashMap<>(); result.put("status",String.valueOf(status)); result.put("msg",msg); httpResponse.getWriter().print(JSONUtils.toJSONString(result)); // 前端可根据返回的status判断 } }
2. 配置拦截器
InterceptorConfig.java
负责将使用了JwtUtil的拦截器配置进入Spring boot。
@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Resource private LoginInterceptor interceptor; @Override public void addInterceptors(InterceptorRegistry registry) { List<String> patterns = new ArrayList<>(); // 添加过滤路由 默认拦截所有请求 patterns.add("/**"); registry.addInterceptor(interceptor) .addPathPatterns(patterns) .excludePathPatterns("/user/login"); // 用户登录请求不拦截 } }
Part2:shiro+jwt
- 明确shiro是什么
- shiro的工作流程
- spring boot如何配置shiro框架
- shiro和jwt的整合
shiro是一个权限管理框架,相对于Spring Security而言,代码简单。适用场景多,不只局限于Java。 shiro的架构原理图:
- Subject:当前和系统交互的"用户",可以是人,系统,第三方插件等。统称为Subject。 SecurityManager:类似于Spring容器的一个容器,是Shiro的核心。管理众多的组件。
- Authenticator:认证组件,用户需要先通过系统认证,在进行用户授权,需要先判断系统中是否有这个用户,在进行后续操作,因此在这里就是进行系统认证的地方。
- Authorizer:授权,当一个用户是属于当前系统时,这个用户的一些操作就需要判断是否有权力去做这件事情。在这里就需要进行授权相关的
- Realm 可以理解为数据源,就是在realm记录了那些属于本系统的用户,他们具有什么样的角色
就相当于在一个拥有多个公司的工业园区,人们需要有这个园区的卡片,才允许进入园区,而进入园区之后需要由本公司的门禁你才能进入公司,否则就不能进入公司一样。A公司的员工不能进入B公司。 Subject
就是员工,或者快递小哥, SecurityManager
就是园区门禁系统, Authenticator
就是门禁的闸机 Authorizer
的作用就是公司的门禁一样, realm
就是园区系统的数据库,记录了系统的用户和权限信息 。
工作流程:一个subject通过login()方法,将subject的信息提交给SecurityManager,SecurityManager调用自己的组件去判断,认证,授权等。shiro是通过filter来进行拦截请求的,因此在结合jwt时,就不需要interceptor就能达到预期的效果。
思路流程图:
第一步改造JwtToken
// AuthenticationToken 是shiro框架的。 public class JWTToken implements AuthenticationToken { private String token; public JWTToken(String token) { this.token = token; } public String getToken() { return token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
JWTUtils代码不变,创建和验证逻辑如上。 第二步编写自己的数据源Realm
public class MyRealm extends AuthorizingRealm { // 指定凭证匹配器。匹配器工作在认证后,授权前。 public MyRealm() { this.setCredentialsMatcher(new JWTCredentialsMatcher()); } @Resource UserServiceInt userServiceInt; // 判断token是否为JWTToken 必须重写 @Override public boolean supports(AuthenticationToken token) { return token instanceof JWTToken; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { log.info("AuthenticationInfo 开始认证"); String token = ((JWTToken) authenticationToken).getToken(); String username = JWT.decode(token).getClaim("username").asString(); // 从系统的数据库查找是否拥有这个用户,也可以提前把数据加载到Redis中,从redis中查找即可。 UserModel user = userServiceInt.getUserByUsername(username); if (user == null) { log.info("user为空"); // 认证不通过 return null; } SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo( user, token, "myRealm" ); return simpleAuthenticationInfo; } // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { log.info("开始授权..."); // 从PrincipalCollection获取user UserModel userModel = (UserModel) principalCollection.getPrimaryPrincipal(); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); // 模拟数据库操作。实际上可以利用mybatis的级联查询,查询出用户的角色和权限信息。 if (userModel.getUsername().equals("super")){ // 添加用户角色 simpleAuthorizationInfo.addRole("admin"); // 添加用户权限 simpleAuthorizationInfo.addStringPermission("user:list"); } return simpleAuthorizationInfo; } }
public class JWTCredentialsMatcher implements CredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) { String token = ((JWTToken)authenticationToken).getToken(); log.info("JWTCredentialsMatcher token = {}",token); UserModel userModel = (UserModel) authenticationInfo.getPrincipals().getPrimaryPrincipal(); log.info("JWTCredentialsMatcher token = {}",userModel.toString()); // 调用JwtUtils验证token即可 return JWTUtil.verifyToken(token, userModel.getUsername(), userModel.getPassword()); } }
第三:编写filter拦截前端请求
public class JwtFilter extends BasicHttpAuthenticationFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { log.info("isAccessAllowed : 验证是否拥有token"); String token = ((HttpServletRequest) request).getHeader("token"); HttpServletResponse servletResponse = (HttpServletResponse) response; if (!StringUtils.hasLength(token)) { try { setReturnInfo(servletResponse, 401, "token为空"); } catch (IOException e) { e.printStackTrace(); return false; } } return executeLogin(request,response); } @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) { log.info("executeLogin : 执行登录"); HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; String token = httpServletRequest.getHeader("token"); JWTToken jwtToken = new JWTToken(token); // 提交给realm进行登入,如果错误他会抛出异常并被捕获 try { getSubject(request, response).login(jwtToken); } catch (Exception e) { log.info("认证出现异常:{}", e.getMessage()); try { setReturnInfo(httpServletResponse,401,"token错误"); } catch (IOException ex) { ex.printStackTrace(); } return false; } // 如果没有抛出异常则代表登入成功,返回true return true; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { log.info("登录失败"); return super.onAccessDenied(request, response); } /** * 对跨域提供支持 */ @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); httpServletResponse.setCharacterEncoding("UTF-8"); // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态 if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return true; } return super.preHandle(request, response); } private static void setReturnInfo(HttpServletResponse response, int status, String msg) throws IOException { response.setContentType("application/json;charset=utf-8"); Map<String, String> result = new HashMap<>(); result.put("status", String.valueOf(status)); result.put("msg", msg); response.getWriter().write(JSONUtils.toJSONString(result)); }
第四:配置shiro
@Configuration public class ShiroConfig { @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, Filter> filterMap = new HashMap<>(); filterMap.put("jwt",new JwtFilter()); shiroFilterFactoryBean.setFilters(filterMap); Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/user/login","anon"); filterChainDefinitionMap.put("/**", "jwt"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } // 禁用session @Bean protected SessionStorageEvaluator sessionStorageEvaluator(){ DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator(); sessionStorageEvaluator.setSessionStorageEnabled(false); return sessionStorageEvaluator; } @Bean("securityManager") public SecurityManager securityManager(MyRealm userRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 指定SecurityManager的域 securityManager.setRealm(userRealm); /* * 关闭shiro自带的session,详情见文档 * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29 */ DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO); return securityManager; } @Bean("myRealm") public MyRealm shiroRealm() { MyRealm shiroRealm = new MyRealm(); return shiroRealm; } @Bean public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); autoProxyCreator.setProxyTargetClass(true); return autoProxyCreator; } /** * 开启shiro aop注解支持. * 使用代理方式;所以需要开启代码支持; * @param * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }
第五步在controller的方法上适用shiro权限控制的注解即可
@GetMapping("list") // @RequiresAuthentication @RequiresRoles(value = {"admin"}) // @RequiresPermissions(value = {"user:list"}) public List<UserModel> listUsers(){ return userServiceInt.listUser(); }
到此这篇关于Springboot+Shiro+Jwt实现权限控制的项目实践的文章就介绍到这了,更多相关Springboot Shiro Jwt实现权限控制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!