# shiro学习 **Repository Path**: wangzhaoyv/shiro_learning ## Basic Information - **Project Name**: shiro学习 - **Description**: shrio学习项目 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-04-16 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 代码改变位置 ``` xml 4.0.0 org.springframework.boot spring-boot-starter-parent 2.2.6.RELEASE com.wzy shiro-study 0.0.1-SNAPSHOT shiro-study Demo project for Spring Boot 1.8 mysql mysql-connector-java 8.0.19 com.alibaba druid-spring-boot-starter 1.1.10 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine com.baomidou mybatis-plus-boot-starter 3.3.1.tmp com.baomidou mybatis-plus-generator 3.3.1.tmp org.apache.velocity velocity-engine-core 2.2 com.google.guava guava 23.0 org.apache.shiro shiro-spring 1.3.2 com.auth0 java-jwt 3.2.0 org.springframework.boot spring-boot-maven-plugin ``` ## 图片说明 ![image-20200417102631506](./pic/1.png) ### JWTUtil.java ```java package com.wzy.shirostudy.utils; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.JWTVerifier; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.UnsupportedEncodingException; import java.util.Date; /** * @program: shiro-study * @description: JWT工具 * @author: 1 * @create: 2020-04-16 22:21 **/ public class JWTUtil { final static Logger logger = LogManager.getLogger(JWTUtil.class); /** * 过期时间30分钟 */ private static final long EXPIRE_TIME = 30 * 60 * 1000; /** * 校验token是否正确 * * @param token 密钥 * @param secret 用户的密码 * @return 是否正确 */ public static boolean verify(String token,String username,Integer userId, String secret) { try { String id = String.valueOf(userId); Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withClaim("userId", id) .withClaim("username", username) .build(); verifier.verify(token); return true; } catch (Exception exception) { return false; } } /** * 获得token中的信息无需secret解密也能获得 * * @return token中包含的用户名 */ public static String getUsername(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); } catch (JWTDecodeException e) { return null; } } /** * 获得token中的信息无需secret解密也能获得 * * @return token中包含的用户id */ public static Integer getUserId(String token) { try { DecodedJWT jwt = JWT.decode(token); return Integer.valueOf(jwt.getClaim("userId").asString()); } catch (JWTDecodeException e) { return null; } } /** * 生成签名,30min后过期 * * @param username 用户名 * @param secret 用户的密码 * @return 加密的token */ public static String sign(Integer userId ,String username, String secret) { try { Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); //使用用户自己的密码充当加密密钥 Algorithm algorithm = null; algorithm = Algorithm.HMAC256(secret); // 附带userId信息 注意这个是必须要变为string类型后才可以放入构造者里去,否则后面无法取到值,存入userId也是因为我数据表中,userId比username有用 String id = String.valueOf(userId); // 建造者模式 String jwtString = JWT.create() .withClaim("userId", id) .withClaim("username", username) .withExpiresAt(date) .sign(algorithm); logger.debug(String.format("JWT:%s", jwtString)); return jwtString; } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return null; } } ``` ## JwtToken.java ```java package com.wzy.shirostudy.jwt; import org.apache.shiro.authc.AuthenticationToken; /** * @program: shiro-study * @description: 存取用户名及密码 * @author: 1 * @create: 2020-04-16 16:39 **/ 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; } } ``` ### ShrioCustomRealm.java ```java package com.wzy.shirostudy.configurer; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.wzy.shirostudy.domain.SysPermission; import com.wzy.shirostudy.domain.SysRole; import com.wzy.shirostudy.domain.SysUser; import com.wzy.shirostudy.jwt.JwtToken; import com.wzy.shirostudy.service.SysPermissionService; import com.wzy.shirostudy.service.SysRoleService; import com.wzy.shirostudy.service.SysUserService; import com.wzy.shirostudy.utils.JWTUtil; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.SecurityUtils; 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 com.google.common.collect.Sets; import org.springframework.context.annotation.Configuration; import javax.annotation.Resource; import java.util.List; import java.util.Set; /** * @program: shiro-study * @description: 权限认证 * @author: 1 * @create: 2020-04-15 18:15 **/ @Slf4j @Configuration public class ShiroCustomRealm extends AuthorizingRealm { @Resource private SysUserService sysUserServiceImpl; @Resource private SysRoleService sysRoleServiceImpl; @Resource private SysPermissionService sysPermissionServiceImpl; /** * 必须重写此方法,不然Shiro会报错 */ @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } /** * 对用户进行授权 第一次请求的时候就会启动该方法 * 注意:这里我在生成token的时候放入了userId所以这里才可以取userId * 获取权限这里基本没有改变,就是这里获取的是token 然后解密提取其中的userId * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { String token = (String) SecurityUtils.getSubject().getPrincipal(); Integer userId = JWTUtil.getUserId(token); log.info("--------获取权限---------- : " + token); /** * 获取用户的角色列表 */ Set roles = Sets.newHashSet(); List roleList = sysRoleServiceImpl.getRoleByUserId(userId); roleList.forEach(role -> { roles.add(role.getName()); }); //设置用户角色 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles); // 获得该用户角色权限列表 List permissionList = sysPermissionServiceImpl.getPermissionListByUserId(userId); Set permissions = Sets.newHashSet(); permissionList.forEach(permission -> { if (permission.getPermission() != null && !"".equals(permission.getPermission()) { permissions.add(permission.getPermission()); } }); //设置角色权限 info.setStringPermissions(permissions); return info; } /** * 获取身份验证信息 * shiro会通过 Realm 来获取应用程序中的用户,角色及权限信息 * * 注意,这里本是Cookies版本中的登录环节,但是在jwt版本中,登录接口并不调用此处的登录 * 后面在controller里可以看到,这里拿到token后的登录 * * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String token = (String) authenticationToken.getCredentials(); // 解密获得username,用于和数据库进行对比 Integer userId = JWTUtil.getUserId(token); //没有登录用户信息直接返回 if (userId == null) { throw new AuthenticationException("token invalid"); } //使用登录名获取用户信息 QueryWrapper queryWrapper = new QueryWrapper(); queryWrapper.eq("id", userId); SysUser sysUser = sysUserServiceImpl.getOne(queryWrapper); //如果有查询到用户信息,就将userId转存到token的username中,这样为后面获取数据提供便利 if (!JWTUtil.verify(token,sysUser.getUsername(), sysUser.getId(), sysUser.getPassword())) { throw new AuthenticationException("Username or password error"); } return new SimpleAuthenticationInfo(token, token, "shiro_custom_realm"); } } ``` ### JwtFilter.java ```java package com.wzy.shirostudy.filter; import com.wzy.shirostudy.jwt.JwtToken; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.web.filter.AccessControlFilter; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @url https://www.cnblogs.com/dadiwm321/p/shiro_jwt.html * @program: shiro-study * @description: 自定义一个Filter,用来拦截所有的请求判断是否携带Token * isAccessAllowed()判断是否携带了有效的JwtToken * onAccessDenied()是没有携带JwtToken的时候进行账号密码登录,登录成功允许访问,登录失败拒绝访问 * @author: 1 * @create: 2020-04-16 20:38 **/ @Slf4j public class JwtFilter extends AccessControlFilter { /** * 1. 返回true,shiro就直接允许访问url * 2. 返回false,shiro才会根据onAccessDenied的方法的返回值决定是否允许访问url * @param request * @param response * @param mappedValue * @return * @throws Exception */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { log.warn("isAccessAllowed 方法被调用"); //这里先让它始终返回false来使用onAccessDenied()方法 return false; } /** * 返回结果为true表明登录通过 * @param servletRequest * @param servletResponse * @return * @throws Exception */ @Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { log.warn("------onAccessDenied 方法被调用"); //这个地方和前端约定,要求前端将jwtToken放在请求的Header部分 //所以以后发起请求的时候就需要在Header中放一个Authorization,值就是对应的Token HttpServletRequest request = (HttpServletRequest) servletRequest; String jwt = request.getHeader("Authorization"); JwtToken jwtToken = new JwtToken(jwt); /* * 下面就是固定写法 * */ try { // 委托 realm 进行登录认证 //所以这个地方最终还是调用JwtRealm进行的认证 getSubject(servletRequest, servletResponse).login(jwtToken); //也就是subject.login(token) } catch (Exception e) { e.printStackTrace(); onLoginFail(servletResponse); //调用下面的方法向客户端返回错误信息 return false; } return true; //执行方法中没有抛出异常就表示登录成功 } /** * 登录失败时默认返回 401 状态码 * @param response * @throws IOException */ private void onLoginFail(ServletResponse response) throws IOException { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); httpResponse.getWriter().write("token 令牌失效"); } } ``` > 原ShiroUserFilter ====> JWTFilter > > 原filter功能只做 : 跨域处理0 > > 现在就要做: shiro登录认证授权 > > 那么问题来了,跨域呢??? > > 还记得那个废弃的filter吗?当时就是因为做了跨域,始终都进不了登录验证,可能是我写的有问题,不过跨域问题先放一下吧! > > 可能是我没有用return super.preHandle(request, response);这个返回,直接返回true,但是这里返回这个就报错,而且这个就是个boolean值,应该不影响的,唉!无奈 ## ShrioConfig.java ```java package com.wzy.shirostudy.configurer; import com.wzy.shirostudy.exception.MyExceptionResolver; import com.wzy.shirostudy.filter.JwtFilter; import com.wzy.shirostudy.filter.JwtFilterNo; import org.apache.shiro.mgt.DefaultSessionStorageEvaluator; import org.apache.shiro.mgt.DefaultSubjectDAO; import javax.servlet.Filter; import java.util.HashMap; import java.util.Map; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; /** * @program: shiro-study * @description: shiro的配置文件 * @author: 1 * @create: 2020-04-15 16:01 **/ @Configuration public class ShiroConfigurer { /** * 下面的代码是添加注解支持 */ @Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); // 强制使用cglib,防止重复代理和可能引起代理出错的问题 // https://zhuanlan.zhihu.com/p/29161098 defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } /** * 注入 securityManager */ @Bean public DefaultWebSecurityManager securityManager(ShiroCustomRealm shiroCustomRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 设置realm securityManager.setRealm(shiroCustomRealm); /** * 关闭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; } /** * 将shiro过滤器交给spring管理 * @param securityManager * @return */ @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); //加入 安全管理器 管理所有Subject 相当于整体的管理人。 factoryBean.setSecurityManager(securityManager); /** * 设置无权访问的地址 当使用注解时这个地址就会失效,所以需要重写异常类 * 可查看 {@link MyExceptionResolver} */ factoryBean.setUnauthorizedUrl("/permission/not"); // 添加自己的过滤器并且取名为jwt Map filterMap = new HashMap<>(4); filterMap.put("jwt", new JwtFilter()); factoryBean.setFilters(filterMap); //基本系统级别权限配置 Map filterRuleMap = new HashMap<>(); // druid filterRuleMap.put("/druid/**", "anon"); // 开放登陆接口 filterRuleMap.put("/user/login", "anon"); // 所有请求通过我们自己的JWT Filter 这里没有配置会导致配置 // org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'GET' not supported filterRuleMap.put("/**", "jwt"); factoryBean.setFilterChainDefinitionMap(filterRuleMap); return factoryBean; } } ``` > 配置文件的改变并不大,就是将jwtFilter加入到shiro的过滤器队列中 > > 关闭shiro自带的session > 到这里就基本结束了,尝试一下,发现并不是这么回事,咦,怎么登不上去呢! > > 哦,原来登录逻辑也要稍微改动一下的,在前面的Cookie版本中我们在登录时候获取了shrio的token,然后使用subject.login(token);登录,这个时候就已经走了realm的登录逻辑. > 我们现在修改成这样子,走realm,会显示未登录(请求头没有带Authorization),所以根本走不通,所以我们放弃了以前的登录,改为新的 ## SystemUserController.java ```java package com.wzy.shirostudy.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.wzy.shirostudy.domain.SysUser; import com.wzy.shirostudy.service.SysUserService; import com.wzy.shirostudy.utils.JWTUtil; import com.wzy.shirostudy.utils.MD5Utils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.ExcessiveAttemptsException; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.subject.Subject; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; /** *

* 前端控制器 *

* * @author wzy * @since 2020-04-16 */ @Slf4j @RestController @RequestMapping("/user") public class SysUserController { @Resource private SysUserService sysUserServiceImpl; @PostMapping("/old/login") public String userOldLogin(String username, String password) { // 从SecurityUtils里边创建一个 subject Subject subject = SecurityUtils.getSubject(); // 在认证提交前准备 token(令牌) UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { subject.login(token); } catch (UnknownAccountException unknownAccountException) { return "未知账户"; } catch (IncorrectCredentialsException incorrectCredentialsException) { return "密码不正确"; } catch (LockedAccountException lockedAccountException) { return "用户已锁定"; } catch (ExcessiveAttemptsException excessiveAttemptsException) { return "用户名或密码错误次数过多"; } catch (AuthenticationException authenticationException) { return "用户名或密码不正确"; } if (subject.isAuthenticated()) { return "登录成功"; } else { token.clear(); return "登录失败"; } } @PostMapping("/login") public String userLogin(String username, String password) { log.info("用户登录"); if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { throw new RuntimeException("用户名和密码不可以为空!"); } // 从数据库中根据用户名查找该用户信息 QueryWrapper queryWrapper = new QueryWrapper(); queryWrapper.eq("username", username); SysUser sysUser = sysUserServiceImpl.getOne(queryWrapper); // 登录密码加密 password = MD5Utils.md5(password); if (null != sysUser && sysUser.getPassword().equals(password)) { return JWTUtil.sign(sysUser.getId(),username, password); } else { throw new UnauthorizedException("用户名或者密码错误!"); } } @GetMapping("/info") @RequiresPermissions("sys:user:query") public String testUserInfo() { return "恭喜获取到用户信息"; } @GetMapping("/add") @RequiresPermissions("sys:user:add") public String testUserAdd() { return "恭喜能添加用户"; } @GetMapping("/del") @RequiresPermissions("sys:test:del") public String testUserDel() { return "这是个没有权限的接口"; } @GetMapping("/no/login") public String testNoLogin() { return "你没有登录"; } } ``` > 可以看到,我们其实只是走了数据库,然后通过jwt帮我们使用userId等生成了一个token,返回给了前端,这个时候其实还没有触及到realm的验证逻辑,这个时候我们的jwt接入算是基本完成了,就剩下最后一个跨域问题没有解决了 ## 最后,我们把跨域的问题解决掉 我们在新建一个CorsFilter继承BasicHttpAuthenticationFilter ## CorsFilter.java ```java package com.wzy.shirostudy.filter; import lombok.extern.slf4j.Slf4j; 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; /** * @program: shiro-study * @description: 跨域请求过滤器 * @author: 1 * @create: 2020-04-17 13:09 **/ @Slf4j public class CorsFilter extends BasicHttpAuthenticationFilter { /** * 对跨域提供支持 */ @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { log.warn("cros跨域完成"); 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");//标识允许的请求方法 // 响应首部 Access-Control-Allow-Headers 用于 preflight request (预检请求)中,列出了将会在正式请求的 Access-Control-Expose-Headers 字段中出现的首部信息。修改为请求首部 httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); //给option请求直接返回正常状态 if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return true; } } ``` 然后将这个filter挂载到我们的shrio里去,这样应该就解决了跨域的问题 ```java /** * 将shiro过滤器交给spring管理 * @param securityManager * @return */ @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); //加入 安全管理器 管理所有Subject 相当于整体的管理人。 factoryBean.setSecurityManager(securityManager); /** * 设置无权访问的地址 当使用注解时这个地址就会失效,所以需要重写异常类 * 可查看 {@link MyExceptionResolver} */ factoryBean.setUnauthorizedUrl("/permission/not"); // 添加自己的过滤器并且取名为jwt Map filterMap = new HashMap<>(4); filterMap.put("jwt", new JwtFilter()); //添加跨域过滤器 filterMap.put("core", new CorsFilter()); factoryBean.setFilters(filterMap); //基本系统级别权限配置 Map filterRuleMap = new HashMap<>(); // druid filterRuleMap.put("/druid/**", "anon"); // 开放登陆接口 filterRuleMap.put("/user/login", "anon"); // 所有请求通过我们自己的JWT Filter 这里没有配置会导致配置 // org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'GET' not supported //所有请求经过这两个过滤器 filterRuleMap.put("/**", "core, jwt"); factoryBean.setFilterChainDefinitionMap(filterRuleMap); return factoryBean; } ``` 参考链接: ```html https://blog.csdn.net/qq_40585384/article/details/105345435 ``` ``` https://www.cnblogs.com/sxdcgaq8080/p/6744371.html ``` ``` https://www.cnblogs.com/dadiwm321/p/shiro_jwt.html ```