# taotingbao-security **Repository Path**: lexapps/taotingbao-security ## Basic Information - **Project Name**: taotingbao-security - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2019-03-22 - **Last Updated**: 2020-12-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 该模块适用于前后端分离+分布式下的session共享的权限项目 登入token验证为请求头中的Bearer sessionId, session存储于redis中,做到单点登入 > tip:需要用到登入(表单登入,手机号验证码登入,社交登入)以及验证码模块(放于redis中,已经有保存和校验逻辑)的直接引入依赖即可 > 文档流程 1.使用方法 2.前置配置说明 3.部分关键代码说明 4.业务流程说明 > `2019年3月4日10:13:13 跨域问题解决` ## 1.使用方法 ### (0) 填写yml/properties中的配置文件 company: # 登入时候的分隔符,比如form:eage:123 代表form账号名密码登入 loginSplit: eage shiroRedisSession: # redis维护一个ip对应一个token,当登入时候判断该ip是否使用过token,如果有就删除之前的,保证一台电脑只有一个token,开启需要uniqueIpAddressEnable开启 uniqueIpAddress: bangteng:eage:shiro:uniqueIpAddress # 配置是否需要一个ip对应一个token 保证一台电脑只有一个token uniqueIpAddressEnable: false # 配置是否需要一个ip对应一个token uniqueIpAddressEnable: false # 登入模块名 web pc admin uniqueWebProject: bangteng-pc # session(redis)的项目名字 name: bangteng:eage:shiro:redisSession port: 6379 host: 121.199.74.72 password: 123456 database: 1 # 和容器session同步时间server: session:timeout: 3600000 timeout: 360000 mobileCode: # 短信验证码存储key前缀 name: bangteng:eage:mobile # 短信验证码存储时间 time: 5 ### (1) 设置shiroConfig 配置需要拦截的路径以及放行路径 ```java import com.eage.security.filter.NoLoginFilter; import com.eage.security.filter.NoPermsFilter; import com.eage.security.filter.NoRolesFilter; import lombok.extern.slf4j.Slf4j; 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 javax.servlet.Filter; import java.util.LinkedHashMap; import java.util.Map; /** * @Author: lex * @Date: 2019/2/17 */ @Configuration @Slf4j public class StoreShiroConfig { @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map filterMap = new LinkedHashMap(); Map filters = shiroFilterFactoryBean.getFilters(); filters.put("authc", new NoLoginFilter());//重要!!!将自定义 的未登入跳转注入shiroFilter中 =>前后端分离 未授权并不是跳页面,而是自定义返回json 即setLoginUrl具体方法重写 filters.put("roles", new NoRolesFilter());//重要!!!将自定义 的权限跳转注入shiroFilter中 =>前后端分离 未授权并不是跳页面,而是自定义返回json 即setUnauthorizedUrl具体方法重写 filters.put("perms", new NoPermsFilter());//重要!!!将自定义 的权限跳转注入shiroFilter中 =>前后端分离 未授权并不是跳页面,而是自定义返回json 即setUnauthorizedUrl具体方法重写 //anon:无需登入 认证访问 //authc: 必须认证才可以访问 //user:如果使用了rememberMe可以直接访问 //perms 该资源必须要有资源权限才可以访问 //role 必须得到角色权限才可以访问 //因为是linkedhashmap 从上往下只需要第一个匹配到相应url就执行相应anon filterMap.put("/store/api/**", "anon"); filterMap.put("/store/shiro/login", "anon"); filterMap.put("/**", "authc"); log.warn("当前系统拦截模式为:{}", filterMap); //未授权下的跳转接口 //shiroFilterFactoryBean.setUnauthorizedUrl("/store/admin/page/noAuth"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap); //shiroFilterFactoryBean.setLoginUrl("/store/shiro/noLoginCode"); return shiroFilterFactoryBean; } } ``` ### (2) 实现认证框架具体业务逻辑 ShiroUserDetailServiceImpl的表单登入方式,验证码登入方式,社交登入模式 #### company代表分隔符 依靠前缀判断登入方式 具体如下 表单 username form:company:eage password 123456 --表示 表单登入账号为 eage ,密码为 123456 验证码 username mobile:company:110 password 9876 --表示 手机验证码登入手机号为 110 ,验证码为 9876 社交登入 username social:company:abcdefg password openId:company:wx --表示 社交登入账号为下的 abcdefg,登入方式为openId下的wx登入(也有可能有些需要开放平台下就需要用openId改为unionId,qq登入就将wx改为qq) ### (3) 登入之后将sessionId作为token返回给前端,前端使用该token放于ajax请求头中即可以完成登入 - 获取sessionId方法 ShiroUtils.getSession().getId() 也可以加上模块名companyProperties.getShiroRedisSession().getUniqueWebProject() 从而知道是什么方式登入 ```java import com.eage.common.domain.wrapper.Wrapper; import com.eage.common.utils.PublicUtil; import com.eage.entity.domain.user.UserInfo; import com.eage.security.domain.ShiroLoginVo; import com.eage.security.exception.PrefixNotFoundException; import com.eage.security.properties.CompanyProperties; import com.eage.security.utils.ShiroUtils; import com.eage.service.inter.UserInfoService; import com.eage.store.controller.base.BaseController; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; 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.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; 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 java.io.Serializable; /** * @Author: lex * @Date: 2019/2/19 * @Desc 登入注册 */ @RestController @RequestMapping("store/shiro") @Slf4j public class LoginController extends BaseController { @PostMapping("login") public Wrapper login(@Validated @RequestBody ShiroLoginVo shiroLoginVo, BindingResult bindingResult) { this.validate(bindingResult); Subject subject = SecurityUtils.getSubject(); //封装用户数据 UsernamePasswordToken token = new UsernamePasswordToken(shiroLoginVo.getUsername(), shiroLoginVo.getPassword()); try { subject.login(token); Serializable sessionId = ShiroUtils.getSession().getId(); log.warn("sessionId=[ {} ] ", sessionId); //String jwtToken = JwtUtils.createJWT(sessionId.toString(), ShiroUtils.getUser().toString(), companyProperties.getShiroRedisSession().getTimeout() * 1000); UserInfo userInfo = userInfoService.getById(this.getLoginUserId()); //上一次登入保存的token,做到一个账号只能登入一次,挤下线功能 String oldToken = userInfo.getToken(); //保存在redisSession里面的key前缀[用户的sessionId] String shiroPrefix = companyProperties.getShiroRedisSession().getName() + ":" + companyProperties.getShiroRedisSession().getUniqueWebProject() + ":"; userInfo.setToken(shiroPrefix + sessionId.toString()); log.warn("sessionId=[ {} ] ", userInfo.getToken()); userInfoService.update(userInfo); //删除上一个已经登入使用过的token if (PublicUtil.isNotEmpty(oldToken)) { log.error("删除之前登入的token:{}", oldToken); redisTemplate.delete(oldToken); } log.warn("========================= 登入成功 ========================="); mini userInfo.setToken(StringUtils.substringAfter(userInfo.getToken(), companyProperties.getShiroRedisSession().getName() + ":")); return handleResult(userInfo); } catch (PrefixNotFoundException e) { log.error("登入前缀错误:[ {} ]", e.getMessage(), e); return errorResult("登入前缀错误"); } catch (UnknownAccountException e) { //用户名不存在 log.error("用户名不存在", e); return errorResult("用户名不存在"); } catch (IncorrectCredentialsException e) { //用户名不存在 log.error("密码错误", e); return errorResult("密码错误"); } catch (AuthenticationException e) { log.error("未知异常", e); return errorResult(e.getMessage()); } } } ``` ## 2.前置配置说明 ### (1)主要配置在config(shiro定义配置)/filter(前后端分离需要未登入/未授权重定向方法改为转发)包中 - DefaultShiroConfig shiro主体配置 `主要i 自定义jSessionId获取方式 ii 配置redis保存session` #### 配置自定义jSessionId获取方式 ```java /** * ### deleteInvalidSessions (需要sessionValidationSchedulerEnabled 开启) 失效后是否删除相应的session(redis)中的key 配合globalSessionTimeout ### *

* ### sessionManager.globalSessionTimeout:(需要sessionValidationSchedulerEnabled 开启) 设置全局会话超时时间,默认30分钟,即如果30分钟内没有访问会话将过期; * ***解释: 首先shiro内部算法类似Map. sessionId对应session(redis)的key 单位时间内删除shiro内部算法的sessionId和session(redis)绑定关系.也就是说redis即使设置很久,但是这个globalSessionTimeout只设置10秒. 10秒之后shiro内部和session(redis的key)绑定关系也会被删除 sessionid和session关系解除.所以必须这个时间等于session过期时间,放在redis中就是,这个时间等于redis过期时间*** ### *

* ### sessionManager.sessionValidationSchedulerEnabled:是否开启会话验证器,默认是开启的; *

* sessionValidationScheduler:会话验证调度器,sessionManager默认就是使用ExecutorServiceSessionValidationScheduler,其使用JDK的ScheduledExecutorService进行定期调度并验证会话是否过期; *

* sessionValidationScheduler.interval:设置调度时间间隔,单位毫秒,默认就是1小时; *

* sessionValidationScheduler.sessionManager:设置会话验证调度器进行会话验证时的会话管理器; *

*

*

* sessionManager.sessionValidationScheduler:设置会话验证调度器,默认就是使用ExecutorServiceSessionValidationScheduler。 *

* ### => 这里的做法是globalSessionTimeout过期时间和session(redis)中的过期时间相同 ,session过期的时候redis也同步失效### * * @return */ @Bean public SessionManager sessionManager(SessionDAO sessionDAO) { //配置sessionId获取方式.这里用ajax请求头 MySessionManager mySessionManager = new MySessionManager(companyProperties); //设置sessionId对应session(redis)的key 的绑定关系时间 可以理解为session过期时间.和redis中过期时间最好相同 .否则即使redis设置无限大,也会出异常 //解释: 这个时间表示 sessionId对应session(redis)的key 的绑定关系时间(如果deleteInvalidSessions开启.则会发现 sessionId存在,但是(session)redis 的key一起消失),如果发现redis中的key被删除是因为deleteInvalidSessions 开启了 mySessionManager.setGlobalSessionTimeout(companyProperties.getShiroRedisSession().getTimeout() * 1000); //配置session获得方式 最终是SessionDAO类获取session会话.所以要重写 mySessionManager.setSessionDAO(sessionDAO); return mySessionManager; } ``` #### 配置redis保存session ```java @Bean public SessionDAO sessionDAO(RedisManager redisManager) { log.error("使用redis缓存"); RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager); //shiroRedisSession前缀,保存在redisSession里面的key前缀[用户的sessionId].项目名+模块名,可以让redisManager可视化工具做到每个端分包查看,做到分拣效果而已 String shiroPrefix = companyProperties.getShiroRedisSession().getName() + ":" + companyProperties.getShiroRedisSession().getUniqueWebProject() + ":"; redisSessionDAO.setKeyPrefix(shiroPrefix); log.warn("shiroPrefix:[ {} ]", shiroPrefix); log.info("设置redisSessionDAO"); return redisSessionDAO; } @Bean public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(companyProperties.getShiroRedisSession().getHost() + ":" + companyProperties.getShiroRedisSession().getPort()); redisManager.setPassword(companyProperties.getShiroRedisSession().getPassword()); redisManager.setTimeout(companyProperties.getShiroRedisSession().getTimeout());// 配置过期时间 redisManager.setDatabase(companyProperties.getShiroRedisSession().getDatabase()); log.warn("配置redis连接设置###" + companyProperties.getShiroRedisSession().getHost() + "###" + companyProperties.getShiroRedisSession().getPort() + "###" + companyProperties.getShiroRedisSession().getDatabase()); return redisManager; } ``` #### 配置自定义未登入和未授权返回方式 > 原本未登入未授权是由ShiroFilterFactoryBean的 shiroFilterFactoryBean.setUnauthorizedUrl("/admin/page/noAuth"); shiroFilterFactoryBean.setLoginUrl("/shiro/mp/nologinCode");控制, 但是这是重定向.如果页面控制器在后台手中无所谓,但是前后端分离下无法捕捉到重定向,所以要重写未登入未授权返回方式 - filter包中NoLoginFilter 未登入跳转 - NoPermsFilter 没有权限 - NoRolesFilter 没有角色权限 ### (2)自定义配置文件读取在properties包中 主要是配置redis中key的名字 ## 3.部分关键代码说明 > 主要重写方法是获取前端传来的token进行校验 1. 校验token是否按格式传后台校验 1. 校验该token是否存在于redis中 1. 校验ip是否已经使用过token,做到一个ip一个token,即一个浏览器只能登入一个用户.防止session覆盖问题 ```java import com.eage.security.ienum.ShiroErrorCode; import com.eage.security.properties.CompanyProperties; import com.eage.security.utils.ShiroNetworkUtil; import com.eage.security.utils.ShiroPublicUtil; import com.eage.security.utils.ShiroThreadLocalMap; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.apache.shiro.web.servlet.ShiroHttpServletRequest; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.apache.shiro.web.util.WebUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.Serializable; import java.util.concurrent.TimeUnit; /** * 重写shiro获得jsessionId的方法 * 默认从cookie中获得jsessionId ,现从请求头中"Authorization"获得token * 第一步 :自定义sessionId获取类 * * @Author: lex * @Date: 2018/11/16 * 登入解析token 的地方 */ @Slf4j public class MySessionManager extends DefaultWebSessionManager { private static final String AUTHORIZATION = "Authorization"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; @Autowired private StringRedisTemplate redisTemplate; private CompanyProperties companyProperties; //redisSession前缀,组成是项目名:web模块名 来源是defaultShiroConfig的sessionDao配置 private String redisSessionPrefix; //唯一ip地址前缀,一个ip绑定一个token,防止一个浏览器同时登入2个token,session覆盖问题 private String uniqueIpAddressPrefix; //前端传入token前缀,因为在登入时候默认添加了web模块前缀,所以要删除 private String tokenPrefix; public MySessionManager(CompanyProperties companyProperties) { super(); this.companyProperties = companyProperties; //redisSession前缀,组成是项目名:web模块名 来源是defaultShiroConfig的sessionDao配置 this.redisSessionPrefix = companyProperties.getShiroRedisSession().getName().concat(":").concat(companyProperties.getShiroRedisSession().getUniqueWebProject()).concat(":"); //唯一ip地址前缀,一个ip绑定一个token,防止一个浏览器同时登入2个token,session覆盖问题 this.uniqueIpAddressPrefix = companyProperties.getShiroRedisSession().getUniqueIpAddress().concat(":"); //前端传入token前缀,因为在登入时候默认添加了web模块前缀,所以要删除 this.tokenPrefix = ("Bearer ").concat(companyProperties.getShiroRedisSession().getUniqueWebProject()).concat(":"); } @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { String sessionId = WebUtils.toHttp(request).getHeader(AUTHORIZATION); //如果请求头中有 Authorization 则其值为sessionId sessionId = StringUtils.substringAfter(sessionId, tokenPrefix.toString()); if (!StringUtils.isEmpty(sessionId)) { log.warn("shiro识别jessionId成功 [ {} ]", sessionId); //jwt验证 --省略 sessionId = this.validateRedisSessionId(sessionId, request); if (StringUtils.isBlank(sessionId)) { return null; } request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return sessionId; } else { ShiroThreadLocalMap.put("errorMsg", ShiroErrorCode.CODE_NO_HEAD_TOKEN_ERROR); return null; } } /** * 1.判断redis中是否有相应的jsessionId,没有就未登入 * 2.判断当前token是否已经存在ip对应redis中,无论有无,都刷新,如果有的同时,从redis中取出当前ip对应之前的token,如果不一样就删除对应的redisJsessionId * * @param sessionId * @param request * @return */ private String validateRedisSessionId(String sessionId, ServletRequest request) { //校验session(redis)是否失效 String redisSessionKey = redisSessionPrefix.concat(sessionId).toString(); if (redisTemplate.opsForValue().get(redisSessionKey) == null) { log.error("登入失败.redis没有相应的key:[ {} ]", redisSessionKey); ShiroThreadLocalMap.put("errorMsg", ShiroErrorCode.CODE_NO_REDIS_ERROR); return null; } else { try { //获取当前登入ip String nowIpAddress = uniqueIpAddressPrefix.concat(ShiroNetworkUtil.getIpAddress((HttpServletRequest) request)).toString(); //拿到当前ip对应的旧的存在redis中的redisSessionId,判断和当前登入的redisSessionId是否相同,不同就删除,无论相同与否,都要吧当前的RedisSessionId放入里面 String oldIpAddressRedisSessionKey = redisTemplate.opsForValue().get(nowIpAddress); //刷新用 的当前新ip对应的token redisTemplate.opsForValue().set(nowIpAddress, redisSessionKey, companyProperties.getShiroRedisSession().getTimeout(), TimeUnit.SECONDS); //如果redis查询到对应的session在判断该浏览器已经有一个token,如果有就删除之前的token,防止token覆盖情况,如token 123对应的user信息是aaa,token对应 345对应bbb,再同一浏览器共同使用可能会发生token 123原本对应aaa改成bbb if (ShiroPublicUtil.isNotEmpty(oldIpAddressRedisSessionKey) && !oldIpAddressRedisSessionKey.equals(redisSessionKey)) { redisTemplate.delete(oldIpAddressRedisSessionKey); log.error("浏览器重复使用sessionId,sessionId冲突已经删除旧为 :[ {} ],新为:[ {} ] ", oldIpAddressRedisSessionKey, redisSessionKey); } } catch (IOException e) { ShiroThreadLocalMap.put("errorMsg", ShiroErrorCode.CODE_SYSTEM_ERROR); return null; } } return sessionId; } } ``` ## 4.业务流程说明 > 前端传入前端传入username 进入UserRealm 的doGetAuthenticationInfo 方法.依照相应的前缀进入不同的UserProviderImpl(用户名密码登入,验证码,社交登入) 之后UserProviderImpl去调用shiroUserDetailService 方法.shiroUserDetailService是没有实现类的,需要开发者实现具体步骤, 也就是自己实现getByMobile getByUserName getBySocialProvider ### (0) 创建UserRealm 时候生成一个 由类名+类地址名的一个map ```java @Autowired private FormUserProviderImpl formUserProviderImpl; @Autowired private SocialUserProviderImpl socialUserProviderImpl; @Autowired private MobileUserProviderImpl mobileUserProviderImpl; @Bean(name = "userRealm") public UserRealm getRealm() { Map userProviderMap = new HashMap(); userProviderMap.put(StringUtils.uncapitalize(formUserProviderImpl.getClass().getSimpleName()), formUserProviderImpl); userProviderMap.put(StringUtils.uncapitalize(socialUserProviderImpl.getClass().getSimpleName()), socialUserProviderImpl); userProviderMap.put(StringUtils.uncapitalize(mobileUserProviderImpl.getClass().getSimpleName()), mobileUserProviderImpl); return new UserRealm(userProviderMap); } ``` ### (1) 前端传入username password,切割后根据前缀匹配相应的UserProviderImpl ```java @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { log.warn("shiro登入框架执行 > ============= > ============= > "); UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String username = token.getUsername(); String[] eages = username.split(":" + companyProperties.getLoginSplit() + ":"); if (eages.length == 1) { throw new PrefixNotFoundException("未写登入前缀错误!!!"); } log.warn("登入方式为:[ {} ] ,主体为:[ {} ]", eages[0], eages[1]); String prefix = eages[0]; //这一步根据前端传来的前缀拼接UserProviderImpl 匹配userProviderMap的具体实现类.之后实现具体实现方法 UserProvider userLogin = userProviderMap.get(prefix.concat("UserProviderImpl")); if (userLogin == null) { throw new PrefixNotFoundException("登入前缀错误!!!"); } return userLogin.doGetAuthenticationInfo(token); } ``` ### (2) serProviderImpl去调用shiroUserDetailService 方法.shiroUserDetailService是没有实现类的,需要开发者实现具体步骤,也就是自己实现getByMobile getByUserName getBySocialProvider ```java @Slf4j @Component public class FormUserProviderImpl implements UserProvider { @Autowired private CompanyProperties companyProperties; @Autowired private ShiroUserDetailService shiroUserDetailService; @Override public AuthenticationInfo doGetAuthenticationInfo(UsernamePasswordToken token) throws AuthenticationException{ log.warn("现在为表单登入"); ShiroUserDetail shiroUserDetail = this.getUserDetail(token); token.setPassword(ShiroEncrypt.md5AndSha(new String(token.getPassword())).toCharArray()); return new SimpleAuthenticationInfo(shiroUserDetail, shiroUserDetail.getPassword(), shiroUserDetail.getUsername()); } @Override public ShiroUserDetail getUserDetail(UsernamePasswordToken token) throws AuthenticationException { String username = token.getUsername().split(":"+ companyProperties.getLoginSplit()+":")[1]; return shiroUserDetailService.getByUserName(username); } } package com.eage.mini.security; import com.eage.common.utils.PublicUtil; import com.eage.common.utils.sdk.WxpayUtil; import com.eage.entity.domain.user.ShiroPermsInfo; import com.eage.entity.domain.user.ShiroSocialInfo; import com.eage.entity.domain.user.UserInfo; import com.eage.entity.mapper.ShiroPermsInfoMapper; import com.eage.entity.mapper.ShiroSocialInfoMapper; import com.eage.entity.mapper.UserInfoMapper; import com.eage.security.domain.ShiroUserDetail; import com.eage.security.exception.PrefixNotFoundException; import com.eage.security.exception.SocialUserNotFoundException; import com.eage.security.properties.CompanyProperties; import com.eage.security.login.ShiroUserDetailService; import com.eage.security.utils.ShiroPublicUtil; import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.AccountException; import org.apache.shiro.authc.AuthenticationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import tk.mybatis.mapper.entity.Example; import java.util.Date; import java.util.List; import java.util.Map; /** * @Author: lex * @Date: 2019/2/1 * @Desc 具体业务实现逻辑 开发者在这里实现ShiroUserDetailService 方法,供给security模块实现 */ @Component @Slf4j @Transactional public class ShiroUserDetailServiceImpl implements ShiroUserDetailService { @Autowired private CompanyProperties companyProperties; @Autowired private UserInfoMapper userInfoMapper; @Autowired private ShiroSocialInfoMapper shiroSocialInfoMapper; @Autowired private ShiroPermsInfoMapper shiroPermsInfoMapper; @Override public ShiroUserDetail getByMobile(String mobile) throws AuthenticationException { Example example = new Example(UserInfo.class); example.createCriteria() .andEqualTo("mobile", mobile); List userInfos = userInfoMapper.selectByExample(example); if (PublicUtil.isEmpty(userInfos)) { log.error("未找到与该手机号相关联的用户 :mobile=[ {} ]", mobile); throw new AccountException("未找到与该手机号相关联的用户"); } return this.successLogin(userInfos.get(0)); } @Override public ShiroUserDetail getByUserName(String username) throws AuthenticationException { Example example = new Example(UserInfo.class); example.createCriteria() .andEqualTo("username", username); List userInfos = userInfoMapper.selectByExample(example); if (PublicUtil.isEmpty(userInfos)) { log.error("未找到该用户名对应的用户 :username=[ {} ]", username); throw new AccountException("未找到该用户名对应的用户"); } return this.successLogin(userInfos.get(0)); } @Override public ShiroUserDetail getBySocialProvider(String itemId, String socialProviderId) throws AuthenticationException { String[] split = socialProviderId.split(":" + companyProperties.getLoginSplit() + ":"); if (split.length != 2) { throw new PrefixNotFoundException("社交登入,没有分隔符"); } String social = split[0]; // openId/unionId String providerId = split[1]; //wx/qq log.warn("社交登入中的: social= [ {} 登入,主体为 {} ],providerId=[ {} ]", social, itemId, providerId); //查找相应的openid和unionid Map wxMsg = WxpayUtil.getWXMsg(itemId); if (ShiroPublicUtil.isEmpty(wxMsg)) { throw new SocialUserNotFoundException("wx解析失败"); } if ("openId".equals(social)) { itemId = wxMsg.get("openid").toString(); } else if ("unionId".equals(social)) { itemId = wxMsg.get("unionid").toString(); } else { throw new PrefixNotFoundException("分隔符解析失败 未知openId登入还是unionId登入"); } Example example = new Example(ShiroSocialInfo.class); example.createCriteria() .andEqualTo("providerId", providerId) .andEqualTo(social, itemId); List shiroSocialInfos = shiroSocialInfoMapper.selectByExample(example); if (PublicUtil.isEmpty(shiroSocialInfos)) { log.error("未找到社交登入的账号 :social=[ {} ],providerId=[ {} ]", social, providerId); throw new SocialUserNotFoundException("未找到对应的社交登入记录"); } long userId = Long.parseLong(shiroSocialInfos.iterator().next().getUserId()); UserInfo userInfo = userInfoMapper.selectByPrimaryKey(userId); return this.successLogin(userInfo); } private ShiroUserDetail successLogin(UserInfo userInfo) throws AuthenticationException { userInfo.setLastLogin(new Date()); userInfoMapper.updateByPrimaryKeySelective(userInfo); Example example = new Example(ShiroPermsInfo.class); example.createCriteria() .andEqualTo(id, userInfo.getId()); List shiroPermsInfos = shiroPermsInfoMapper.selectByExample(example); List perms = Lists.newArrayList(); if (PublicUtil.isNotEmpty(shiroPermsInfos)) { shiroPermsInfos.forEach(obj -> perms.add(obj.getPerms())); userInfo.setPerms(perms); userInfo.setShiroPermsInfoList(shiroPermsInfos); } log.warn(id, userInfo.getId().toString(), userInfo.getUsername()); log.warn("shiro框架识别用户名成功 < ================= < ============== < "); return new ShiroUserDetail(userInfo.getId().toString(), userInfo.getUsername(), userInfo.getMobile(), userInfo.getPassword(), userInfo.getPerms()); } } ``` ```java import com.eage.common.utils.PublicUtil; import com.eage.common.utils.sdk.WxpayUtil; import com.eage.entity.domain.user.ShiroPermsInfo; import com.eage.entity.domain.user.ShiroSocialInfo; import com.eage.entity.domain.user.UserInfo; import com.eage.entity.mapper.ShiroPermsInfoMapper; import com.eage.entity.mapper.ShiroSocialInfoMapper; import com.eage.entity.mapper.UserInfoMapper; import com.eage.security.domain.ShiroUserDetail; import com.eage.security.exception.PrefixNotFoundException; import com.eage.security.exception.SocialUserNotFoundException; import com.eage.security.properties.CompanyProperties; import com.eage.security.login.ShiroUserDetailService; import com.eage.security.utils.ShiroPublicUtil; import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.AccountException; import org.apache.shiro.authc.AuthenticationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import tk.mybatis.mapper.entity.Example; import java.util.Date; import java.util.List; import java.util.Map; /** * @Author: lex * @Date: 2019/2/1 * @Desc 具体业务实现逻辑 开发者在这里实现ShiroUserDetailService 方法,供给security模块实现 */ @Component @Slf4j @Transactional public class ShiroUserDetailServiceImpl implements ShiroUserDetailService { @Autowired private CompanyProperties companyProperties; @Autowired private UserInfoMapper userInfoMapper; @Autowired private ShiroSocialInfoMapper shiroSocialInfoMapper; @Autowired private ShiroPermsInfoMapper shiroPermsInfoMapper; @Override public ShiroUserDetail getByMobile(String mobile) throws AuthenticationException { Example example = new Example(UserInfo.class); example.createCriteria() .andEqualTo("mobile", mobile); List userInfos = userInfoMapper.selectByExample(example); if (PublicUtil.isEmpty(userInfos)) { log.error("未找到与该手机号相关联的用户 :mobile=[ {} ]", mobile); throw new AccountException("未找到与该手机号相关联的用户"); } return this.successLogin(userInfos.get(0)); } @Override public ShiroUserDetail getByUserName(String username) throws AuthenticationException { Example example = new Example(UserInfo.class); example.createCriteria() .andEqualTo("username", username); List userInfos = userInfoMapper.selectByExample(example); if (PublicUtil.isEmpty(userInfos)) { log.error("未找到该用户名对应的用户 :username=[ {} ]", username); throw new AccountException("未找到该用户名对应的用户"); } return this.successLogin(userInfos.get(0)); } @Override public ShiroUserDetail getBySocialProvider(String itemId, String socialProviderId) throws AuthenticationException { String[] split = socialProviderId.split(":" + companyProperties.getLoginSplit() + ":"); if (split.length != 2) { throw new PrefixNotFoundException("社交登入,没有分隔符"); } String social = split[0]; // openId/unionId String providerId = split[1]; //wx/qq log.warn("社交登入中的: social= [ {} 登入,主体为 {} ],providerId=[ {} ]", social, itemId, providerId); //查找相应的openid和unionid Map wxMsg = WxpayUtil.getWXMsg(itemId); if (ShiroPublicUtil.isEmpty(wxMsg)) { throw new SocialUserNotFoundException("wx解析失败"); } if ("openId".equals(social)) { itemId = wxMsg.get("openid").toString(); } else if ("unionId".equals(social)) { itemId = wxMsg.get("unionid").toString(); } else { throw new PrefixNotFoundException("分隔符解析失败 未知openId登入还是unionId登入"); } Example example = new Example(ShiroSocialInfo.class); example.createCriteria() .andEqualTo("providerId", providerId) .andEqualTo(social, itemId); List shiroSocialInfos = shiroSocialInfoMapper.selectByExample(example); if (PublicUtil.isEmpty(shiroSocialInfos)) { log.error("未找到社交登入的账号 :social=[ {} ],providerId=[ {} ]", social, providerId); throw new SocialUserNotFoundException("未找到对应的社交登入记录"); } long userId = Long.parseLong(shiroSocialInfos.iterator().next().getUserId()); UserInfo userInfo = userInfoMapper.selectByPrimaryKey(userId); return this.successLogin(userInfo); } private ShiroUserDetail successLogin(UserInfo userInfo) throws AuthenticationException { userInfo.setLastLogin(new Date()); userInfoMapper.updateByPrimaryKeySelective(userInfo); Example example = new Example(ShiroPermsInfo.class); example.createCriteria() .andEqualTo(id, userInfo.getId()); List shiroPermsInfos = shiroPermsInfoMapper.selectByExample(example); List perms = Lists.newArrayList(); if (PublicUtil.isNotEmpty(shiroPermsInfos)) { shiroPermsInfos.forEach(obj -> perms.add(obj.getPerms())); userInfo.setPerms(perms); userInfo.setShiroPermsInfoList(shiroPermsInfos); } log.warn(id, userInfo.getId().toString(), userInfo.getUsername()); log.warn("shiro框架识别用户名成功 < ================= < ============== < "); return new ShiroUserDetail(userInfo.getId().toString(), userInfo.getUsername(), userInfo.getMobile(), userInfo.getPassword(), userInfo.getPerms()); } } ``` # 2019年3月4日10:13:13 跨域问题解决 ```java import lombok.extern.slf4j.Slf4j; import org.apache.shiro.web.filter.authc.UserFilter; 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; import java.io.IOException; /** * @Author: lex * @Date: 2019/2/26 * @Desc 登入失败, 授权失败 请求地址拦截器. 因为是前后端分离,后台不控制页面跳转 返回登入失败状态码 之后返回前端而不用进行重定向 */ @Slf4j public class NoLoginFilter extends UserFilter { /** * 判断是否跳过跨域,前端token是否验证成功 */ @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletResponse httpResponse = (HttpServletResponse) response; HttpServletRequest httpRequest = (HttpServletRequest) request; //在访问过来的时候检测是否为OPTIONS请求,如果是就直接返回true if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { ShiroURLFunction.setHeader(httpRequest, httpResponse); return true; } return super.preHandle(request, response); } } ```