备忘录九:Spring Boot+Shiro权限管理
一:配置pom.xml文件
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency>
二:ShiroConfig配置类
@Configuration public class ShiroConfig { @Bean("sessionManager") public SessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setGlobalSessionTimeout(24 * 60 * 60 * 1000); // 开启会话验证器 sessionManager.setSessionValidationSchedulerEnabled(true); // 删除失效的session sessionManager.setDeleteInvalidSessions(true); // 指定sessionId,使用默认的“JSESSIONID” sessionManager.setSessionIdCookieEnabled(true); return sessionManager; } /** * 我们在使用shiro的时候,首先都会先初始化SecurityManager, * 然后往SecurityManager中注入shiro的其他组件,像sessionManager、realm等。 */ @Bean("securityManager") public SecurityManager securityManager(OAuth2Realm oAuth2Realm, SessionManager sessionManager) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(oAuth2Realm); securityManager.setSessionManager(sessionManager); return securityManager; } @Bean("shiroFilter") public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); // oauth过滤 Map<String, Filter> filters = new HashMap<>(); filters.put("oauth2", new OAuth2Filter()); shiroFilter.setFilters(filters); // 配置可以匿名访问的地址,可以根据实际情况自己添加,放行一些静态资源等,anon 表示放行 Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/webjars/**", "anon"); filterMap.put("/druid/**", "anon"); filterMap.put("/app/**", "anon"); filterMap.put("/file/**", "anon"); filterMap.put("/shiro/login", "anon"); filterMap.put("/swagger/**", "anon"); filterMap.put("/v2/api-docs", "anon"); filterMap.put("/swagger-ui.html", "anon"); filterMap.put("/swagger-resources/**", "anon"); filterMap.put("/captcha.jpg", "anon"); // 其它通过自定义的OAuth2Filter进行过滤 filterMap.put("/**", "oauth2"); shiroFilter.setFilterChainDefinitionMap(filterMap); return shiroFilter; } @Bean("lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * @Title: defaultAdvisorAutoProxyCreator * @Description: 扫描上下文,寻找所有的Advistor(通知器),将这些Advisor应用到所有符合切入点的Bean中 * @return Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。 * @return DefaultAdvisorAutoProxyCreator 返回类型 * @throws */ // @Bean // public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { // DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator(); // proxyCreator.setProxyTargetClass(true); // return proxyCreator; // } /** * @Title: authorizationAttributeSourceAdvisor * @Description: 开启shiro aop注解支持 * @param securityManager * @return 参数说明 * @return AuthorizationAttributeSourceAdvisor 返回类型 * @throws */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
其中,如果启用了DefaultAdvisorAutoProxyCreator的话,会导致二次代理的问题,Realm中的doGetAuthorizationInfo会重复调用2次。
三:自定义Shiro Filter类OAuth2Filter
public class OAuth2Filter extends AuthenticatingFilter { @Override protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { //获取请求token String token = getRequestToken((HttpServletRequest) request); if(StringUtils.isNullOrEmpty(token)){ return null; } return new OAuth2Token(token); } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { //现在vue项目中使用axios发送http请求,每次请求都会多一次Request Method: OPTIONS请求,称为“预检”请求 if(((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())){ return true; } return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { //获取请求token,如果token不存在,直接返回401 String token = getRequestToken((HttpServletRequest) request); if(StringUtils.isNullOrEmpty(token)){ HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); String json = new Gson().toJson(R.error(HttpStatus.SC_UNAUTHORIZED, "invalid token")); httpResponse.getWriter().print(json); return false; } return executeLogin(request, response); } @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setContentType("application/json;charset=utf-8"); httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); try { //处理登录失败的异常 Throwable throwable = e.getCause() == null ? e : e.getCause(); R r = R.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage()); String json = new Gson().toJson(r); httpResponse.getWriter().print(json); } catch (IOException e1) { } return false; } /** * 获取请求的token */ private String getRequestToken(HttpServletRequest httpRequest){ //从header中获取token String token = httpRequest.getHeader("token"); //如果header中不存在token,则从参数中获取token if(StringUtils.isNullOrEmpty(token)){ token = httpRequest.getParameter("token"); } return "112233445566";//token; } }
对于复杂的跨域请求,Vue会首先发送一个OPTIONS请求,进行验证。需要后端对所有接口统一处理放行OPTIONS方法(即返回200)即可
四:自定义Token
public class OAuth2Token implements AuthenticationToken { private String token; public OAuth2Token(String token) { this.token = token; } /* * 登录提交的用户名 * */ @Override public String getPrincipal() { return token; } /* * 只被Subject 知道的秘密值,比如我们登录提供的密码 * */ @Override public Object getCredentials() { return token; } }
五:自定义OAuth2Realm
@Component public class OAuth2Realm extends AuthorizingRealm { @Autowired private ShiroService shiroService; /* * 判断此Realm是否支持此Token */ @Override public boolean supports(AuthenticationToken token) { return token instanceof OAuth2Token; } /* * 提供用户信息返回权限信息 * */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("=============获取已登录用户,权限信息============"); SysUserEntity user = (SysUserEntity) principals.getPrimaryPrincipal(); Long userId = user.getUserId(); // 用户权限列表 Set<String> permsSet = shiroService.getUserPermissions(userId); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.setStringPermissions(permsSet); return info; } /* * 根据token获取认证信息 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("=============获取已登录用户,用户信息============"); String accessToken = (String) token.getPrincipal(); // 根据accessToken,查询用户信息 SysUserTokenEntity tokenEntity = shiroService.queryByToken(accessToken); // token失效 if (tokenEntity == null) { throw new IncorrectCredentialsException("token失效,请重新登录"); } // 查询用户信息 SysUserEntity user = shiroService.queryUser(tokenEntity.getUserId()); // 账号锁定 if (user.getStatus().equals("00")) { throw new LockedAccountException("账号已被锁定,请联系管理员"); } SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, getName() // 返回一个唯一的Realm名字 ); return info; } }
shiroService用来从数据库或者缓存中,查询用户信息和权限信息。
六:TokenGenerator
public class TokenGenerator { public static String generateValue() { return generateValue(UUID.randomUUID().toString()); } private static final char[] hexCode = "0123456789abcdef".toCharArray(); public static String toHexString(byte[] data) { if(data == null) { return null; } StringBuilder r = new StringBuilder(data.length*2); for ( byte b : data) { r.append(hexCode[(b >> 4) & 0xF]); r.append(hexCode[(b & 0xF)]); } return r.toString(); } public static String generateValue(String param) { try { MessageDigest algorithm = MessageDigest.getInstance("MD5"); algorithm.reset(); algorithm.update(param.getBytes()); byte[] messageDigest = algorithm.digest(); return toHexString(messageDigest); } catch (Exception e) { e.printStackTrace(); } return ""; } }
七:Controller层Demo
@RestController @RequestMapping("shiro") public class ShiroController { @RequestMapping(value = "/login", method = RequestMethod.GET) public String login() { return "login"; } @RequestMapping(value = "/info", method = RequestMethod.GET) @RequiresPermissions("sys:config:info") public String info() { return "info"; } }
八:登录代码
@PostMapping("/sys/login") public Map<String, Object> login(@RequestBody SysLoginForm form)throws IOException { boolean captcha = captchaService.validate(form.getUuid(), form.getCaptcha()); if(!captcha){ return R.error("验证码不正确"); } //用户信息 SysUserEntity user = sysUserService.queryByUserName(form.getUsername()); //账号不存在、密码错误 if(user == null || !user.getPassword().equals(new Sha256Hash(form.getPassword(), user.getSalt()).toHex())) { return R.error("账号或密码不正确"); } //账号锁定 if(Constant.CommonStatus.BANNED.getValue().equals(user.getStatus())){ return R.error("账号已被锁定,请联系管理员"); } //生成token,并保存到数据库 R r = sysUserTokenService.createToken(user.getUserId()); return r; }
登录验证,独立实现,验证成功后创建Token,Token放到Redis缓存中。前端页面请求接口时要带Token.
九:代码调用流程
1.登录成功后,创建Token,后面的接口调用都要传递Token
2.接口首先通过shiroFilter过滤,确定是否要进入OAuth2Filter进行处理
3. OAuth2Filter 首先执行isAccessAllowed方法,如果时OPTIONS请求直接放行,否则进行Shiro的executeLogin验证
4. executeLogin执行的时候会到OAuth2Realm中调用doGetAuthenticationInfo方法,根据Token获取当前登录用户的信息(Token与用户的关联,已在第八点自己实现的登录代码中实现)
5. executeLogin成功后,会看当前访问的接口,有无权限注解@RequiresPermissions
6.如果有注解的话,说需要进行权限验证,Shiro会通过 OAuth2Realm的doGetAuthorizationInfo方法,获取当前用户的权限进行验证
原文地址: http://blog.itpub.net/28624388/viewspace-2660254/
本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
相关文章