diff --git a/README.md b/README.md index ac5f88b..76bdef5 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,8 @@ parent:版本封装
base:功能封装 #### 最新版本 -* 1.x yexuejc.springboot.version=1.1.4
-* 2.x yexuejc.springboot.version=2.0.3
-* yexuejc.base.version=1.2.1 +* yexuejc.springboot.version=1.2.0
+* yexuejc.base.version=1.2.4 pom.xml ``` diff --git a/UPDATE.md b/UPDATE.md index 4ecdf5a..286fe53 100644 --- a/UPDATE.md +++ b/UPDATE.md @@ -1,6 +1,20 @@ yexuejc-springboot 更新内容 ------------------- +#### version :1.2.0 +**time:2018-12-1 12:19:06**
+**branch:** master
+**关联工程:**
+``` +springboot-base:1.2.4 +spring-boot-starter-parent:1.5.16.RELEASE +``` +**update:**
+1. security多方登录第一个稳定版
+支持账号登录、短信登录、第三方授权openid登录
+功能链接[security重构-多方登录](doc/SECURITY.md) +# + #### version :1.1.6-1.1.9 **time:2018-11-21 15:03:01**
**branch:** master
diff --git a/doc/SECURITY.md b/doc/SECURITY.md index 0ad013c..8eeee24 100644 --- a/doc/SECURITY.md +++ b/doc/SECURITY.md @@ -1,5 +1,7 @@ -Security框架封装集成登录 使用指南 +Security框架封装集成多方登录 使用指南 ------------- +#### 先上[效果图](Securtity效果图.md) + 单独使用例子工程:[https://github.com/yexuejc/springboot-security-login-simple](https://github.com/yexuejc/springboot-security-login-simple) * 本项目依赖不向下传递 @@ -15,7 +17,8 @@ Security框架封装集成登录 使用指南 ``` > **相关文件说明** 所有核心文件都在 com.yexuejc.springboot.base.security 包下 - +#### 现附上系统实现逻辑图 +![多方登录系统实现逻辑图](多方登录设计.jpg) 1.com.yexuejc.springboot.base.security.SecurityConfig
@@ -26,6 +29,38 @@ Security框架封装集成登录 使用指南 * 继承configure(HttpSecurity http) 完善更多security过滤配置 * 例子[com.yexuejc.springboot.base.security.MySecurityConfig](../yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/security/MySecurityConfig.java) +#### 注: 代码中抛出的相关异常拦截在filter.setAuthenticationFailureHandler()中处理,参考[MySecurityConfig](../yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/security/MySecurityConfig.java) +``` +filter.setAuthenticationFailureHandler((request, response, exception) -> { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + Resps resps = new Resps(); + if (exception instanceof DisabledException) { + resps.setErr(RespsConsts.CODE_FAIL, new String[]{BizConsts.BASE_IS_LOCK_MSG}); + } else if (exception instanceof AccountExpiredException) { + resps.setErr(RespsConsts.CODE_FAIL, new String[]{BizConsts.BASE_IS_EXPIRE_MSG}); + } else if (exception instanceof CredentialsExpiredException) { + resps.setErr(BizConsts.BASE_LOGIN_IS_EXPIRE_CODE, new String[]{BizConsts.BASE_LOGIN_IS_EXPIRE_MSG}); + } else if (exception instanceof LockedException) { + resps.setErr(RespsConsts.CODE_FAIL, new String[]{BizConsts.BASE_IS_LOCKED_MSG}); + } else if (exception instanceof AuthenticationCredentialsNotFoundException) { + resps.setErr(RespsConsts.CODE_FAIL, new String[]{BizConsts.BASE_CREDENTIALS_NOT_FOUND_MSG}); + } else if (exception instanceof ThirdPartyAuthorizationException) { + resps.setErr(RespsConsts.CODE_FAIL, new String[]{exception.getMessage()}); + } else if (exception instanceof BadCredentialsException) { + resps.setErr(RespsConsts.CODE_FAIL, new String[]{BizConsts.BASE_PWD_IS_ERR_MSG}); + } else if (exception instanceof UsernameNotFoundException) { + resps.setErr(RespsConsts.CODE_FAIL, new String[]{BizConsts.BASE_ACCOUNT_NOT_FOUND_MSG}); + } else if (exception instanceof UserNotAuthoriayException) { + resps.setErr(RespsConsts.CODE_FAIL, new String[]{exception.getMessage()}); + } else { + resps.setErr(RespsConsts.CODE_FAIL, new String[]{BizConsts.BASE_SYS_ERR_MSG}); + } + response.getWriter().write(JsonUtil.obj2Json(resps)); + response.getWriter().close(); + }); +``` + 2.com.yexuejc.springboot.base.security.UserDetailsManager
**获取登录用户信息** diff --git a/doc/Securtity效果图.md b/doc/Securtity效果图.md new file mode 100644 index 0000000..dcdf1af --- /dev/null +++ b/doc/Securtity效果图.md @@ -0,0 +1,24 @@ +Security 多方登录封装使用效果图 +--------------- +### 账号登录 +密码错误 +![](sl/sl_02.png) +正确 +![](sl/sl_01.png) + +### 短信登录 +发送短信 +![](sl/sl_ss.png) +短信错误 +![](sl/sl_err.jpg) +短信过期 +![](sl/sl_gq.png) +正确 +![](sl/sl_10.png) + + +### 第三方登录 +第一次登录,需要绑定手机号 +![](sl/sl_t3.png) +绑定过手机号的第三方账号登录(绑定相关业务需要自己实现) +![](sl/sl_t4.png) \ No newline at end of file diff --git a/doc/sl/sl_01.png b/doc/sl/sl_01.png new file mode 100644 index 0000000..4301520 Binary files /dev/null and b/doc/sl/sl_01.png differ diff --git a/doc/sl/sl_02.png b/doc/sl/sl_02.png new file mode 100644 index 0000000..6325819 Binary files /dev/null and b/doc/sl/sl_02.png differ diff --git a/doc/sl/sl_10.png b/doc/sl/sl_10.png new file mode 100644 index 0000000..f77936a Binary files /dev/null and b/doc/sl/sl_10.png differ diff --git a/doc/sl/sl_err.jpg b/doc/sl/sl_err.jpg new file mode 100644 index 0000000..1910cf0 Binary files /dev/null and b/doc/sl/sl_err.jpg differ diff --git a/doc/sl/sl_gq.png b/doc/sl/sl_gq.png new file mode 100644 index 0000000..9079cd5 Binary files /dev/null and b/doc/sl/sl_gq.png differ diff --git a/doc/sl/sl_ss.png b/doc/sl/sl_ss.png new file mode 100644 index 0000000..842a277 Binary files /dev/null and b/doc/sl/sl_ss.png differ diff --git a/doc/sl/sl_t3.png b/doc/sl/sl_t3.png new file mode 100644 index 0000000..e9ffd05 Binary files /dev/null and b/doc/sl/sl_t3.png differ diff --git a/doc/sl/sl_t4.png b/doc/sl/sl_t4.png new file mode 100644 index 0000000..850d1d8 Binary files /dev/null and b/doc/sl/sl_t4.png differ diff --git a/doc/多方登录设计.jpg b/doc/多方登录设计.jpg new file mode 100644 index 0000000..9f116ab Binary files /dev/null and b/doc/多方登录设计.jpg differ diff --git a/pom.xml b/pom.xml index 5d700b9..9ca6956 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.yexuejc.springboot yexuejc-springboot-parent - 1.1.9 + 1.2.0 pom ${project.artifactId} diff --git a/yexuejc-springboot-base/pom.xml b/yexuejc-springboot-base/pom.xml index 74daa6c..1dc0d30 100644 --- a/yexuejc-springboot-base/pom.xml +++ b/yexuejc-springboot-base/pom.xml @@ -9,7 +9,7 @@ com.yexuejc.springboot yexuejc-springboot-parent - 1.1.9 + 1.2.0 diff --git a/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/security/UserServiceImpl.java b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/security/UserServiceImpl.java index 844aac3..d8b7623 100644 --- a/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/security/UserServiceImpl.java +++ b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/security/UserServiceImpl.java @@ -13,11 +13,12 @@ import com.yexuejc.springboot.base.constant.LogTypeConsts; import com.yexuejc.springboot.base.exception.ThirdPartyAuthorizationException; import com.yexuejc.springboot.base.mapper.ConsumerMapper; import com.yexuejc.springboot.base.security.domain.Consumer; -import com.yexuejc.springboot.base.security.inte.User; import com.yexuejc.springboot.base.security.inte.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @@ -44,28 +45,42 @@ public class UserServiceImpl implements UserService { /** * 根据用户名到数据库查询用户 - *

- * 账号未找到,抛出UsernameNotFoundException异常=>会走第三方登录流程 - *

* * @param username 登录账号 * @return */ @Override - public User getConsumerByUserName(String username) { + public Object getConsumerByUserName(String username) { if (StrUtil.isEmpty(username)) { - throw new UsernameNotFoundException(username); + throw new UsernameNotFoundException("username为空,一般是第三方登录来的,直接抛出UsernameNotFoundException就是"); } QueryWrapper queryWrapper = new QueryWrapper(); queryWrapper.eq("mobile", username); Consumer consumer = consumerMapper.selectOne(queryWrapper); if (null == consumer) { - throw new UsernameNotFoundException(username); + /** + * 1.抛出UsernameNotFoundException这个异常如果是第三方登录会走 {@link #checkOpenId(ConsumerToken)} + * 2.抛出其他Exception可以自己到{@link MySecurityConfig#loginHodler(ConsumerAuthenticationProcessingFilter)} + * 里面的filter.setAuthenticationFailureHandler()中做特殊处理 + */ + throw new UsernameNotFoundException("没有该账号相关信息"); } + //h2不支持json,人为处理角色 ArrayList roles = new ArrayList<>(); roles.add("ROLE_CONSUMER"); consumer.setRoles(roles); - return consumer; + //1.consumer为User的实现类 +// return consumer; + + //2. 自己创建ConsumerUser,直接返回 + List authorities = new ArrayList<>(); + for (String role : consumer.getRoles()) { + authorities.add(new SimpleGrantedAuthority(role)); + } + ConsumerUser consumerUser = new ConsumerUser(consumer.getMobile(), consumer.getPwd(), + consumer.getEnable(), consumer.getNonExpire(), true, consumer.getNonLock(), + authorities, consumer.getConsumerId(), null, System.currentTimeMillis()); + return consumerUser; } /** @@ -98,15 +113,14 @@ public class UserServiceImpl implements UserService { } /** + * 第三方登录 * 校验openid 根据自己业务做判断 - *
- * 返回:封装登录用户信息到 apiVO.setObject1(User.class) 自己封装登录用户信息 * * @param consumerToken 登录信息 * @return */ @Override - public ApiVO checkOpenId(ConsumerToken consumerToken) { + public Object checkOpenId(ConsumerToken consumerToken) { ApiVO apiVO = new ApiVO(ApiVO.STATUS.F, "没有找到用户信息"); switch (consumerToken.getLogtype()) { case LogTypeConsts.QQ: @@ -121,9 +135,89 @@ public class UserServiceImpl implements UserService { default: break; } - return apiVO; + if (apiVO.isFail()) { + /** + * 未查到: + * 1.返回null会走(数据库没有这个openid[第三方账号]信息)新增流程 {@link #addConsumer(ConsumerToken)} + * 2.也可以自己创建一个带有特殊标识的ConsumerUser,然后在 {@link MySecurityConfig#loginHodler(ConsumerAuthenticationProcessingFilter)} + * 里面的filter.setAuthenticationSuccessHandler()中做特殊处理 ps:假装登录成功 :) + */ + return null; + } + //h2不支持json,人为处理角色 + ArrayList roles = new ArrayList<>(); + roles.add("ROLE_CONSUMER"); + apiVO.getObject1(Consumer.class).setRoles(roles); + //根据openid到数据库查到consumer返回 + return apiVO.getObject1(Consumer.class); } + /** + * {@link #checkOpenId(ConsumerToken)} 返回null会走该方法 + * 没有账号时处理自己的业务,此处必须返回 构造出的登录用户,否则会抛出{@link ThirdPartyAuthorizationException 第三方授权异常} + *
+ * + * @param consumerToken 登录信息 + * @return + */ + @Override + public Object addConsumer(ConsumerToken consumerToken) { + Consumer consumer = new Consumer(); + consumer.setConsumerId(StrUtil.genUUID()); + consumer.setMobile(StrUtil.isNotEmpty(consumerToken.getUsername()) ? consumerToken.getUsername() : consumerToken.getOpenid()); + consumer.setPwd(StrUtil.toMD5("123456")); + consumer.setEnable(true); + consumer.setNonExpire(true); + consumer.setNonLock(true); + List roles = new ArrayList<>(); + roles.add("ROLE_CONSUMER"); + consumer.setRoles(roles); + switch (consumerToken.getLogtype()) { + case LogTypeConsts.SMS: + ApiVO apiVO = checkSmsCode2Redis(BizConsts.CONSUMER_LOGIN_SMS, consumerToken.getUsername(), + consumerToken.getSmscode()); + if (apiVO.isFail()) { + throw new ThirdPartyAuthorizationException("短信验证码错误"); + } + consumer.setNickname(consumerToken.getUsername()); + consumer.setHead("/head/def.png"); + consumer.setRegType(DictRegTypeConsts.DICT_MOBILE); + break; + case LogTypeConsts.QQ: + consumer.setQqId(consumerToken.getOpenid()); + consumer.setNickname(consumerToken.getNickname()); + setHeader(consumerToken, consumer, false); + setSex(consumerToken, consumer); + consumer.setRegType(DictRegTypeConsts.DICT_QQ); + break; + case LogTypeConsts.WECHAT: + consumer.setWechatId(consumerToken.getOpenid()); + consumer.setNickname(consumerToken.getNickname()); + setHeader(consumerToken, consumer, false); + setSex(consumerToken, consumer); + consumer.setRegType(DictRegTypeConsts.DICT_WECHAT); + break; + case LogTypeConsts.WEIBO: + consumer.setWeiboId(consumerToken.getOpenid()); + consumer.setNickname(consumerToken.getNickname()); + setHeader(consumerToken, consumer, false); + setSex(consumerToken, consumer); + consumer.setRegType(DictRegTypeConsts.DICT_WEIBO); + break; + default: + throw new ThirdPartyAuthorizationException("暂不支持该第三方授权"); + } + Integer result = consumerMapper.insert(consumer); + if (result < 1) { + /** + * 会抛出{@link ThirdPartyAuthorizationException 第三方授权异常} + */ + return null; + } + return consumer; + } + + /** * 第三方登录 QQ登录 * @@ -183,69 +277,6 @@ public class UserServiceImpl implements UserService { } - /** - * 没有账号时处理自己的业务,此次必须返回 构造出的登录用户,否则会抛出{@link ThirdPartyAuthorizationException 第三方授权异常} - *
- * 返回:封装登录用户信息到 apiVO.setObject1(User.class) 自己封装登录用户信息 - * - * @param consumerToken 登录信息 - * @return - */ - @Override - public ApiVO addConsumer(ConsumerToken consumerToken) { - Consumer consumer = new Consumer(); - consumer.setConsumerId(StrUtil.genUUID()); - consumer.setMobile(StrUtil.isNotEmpty(consumerToken.getUsername()) ? consumerToken.getUsername() : consumerToken.getOpenid()); - consumer.setPwd(StrUtil.toMD5("123456")); - consumer.setEnable(true); - consumer.setNonExpire(true); - consumer.setNonLock(true); - List roles = new ArrayList<>(); - roles.add("ROLE_CONSUMER"); - consumer.setRoles(roles); - switch (consumerToken.getLogtype()) { - case LogTypeConsts.SMS: - ApiVO apiVO = checkSmsCode2Redis(BizConsts.CONSUMER_LOGIN_SMS, consumerToken.getUsername(), - consumerToken.getSmscode()); - if (apiVO.isFail()) { - return apiVO; - } - consumer.setNickname(consumerToken.getUsername()); - consumer.setHead("/head/def.png"); - consumer.setRegType(DictRegTypeConsts.DICT_MOBILE); - break; - case LogTypeConsts.QQ: - consumer.setQqId(consumerToken.getOpenid()); - consumer.setNickname(consumerToken.getNickname()); - setHeader(consumerToken, consumer, false); - setSex(consumerToken, consumer); - consumer.setRegType(DictRegTypeConsts.DICT_QQ); - break; - case LogTypeConsts.WECHAT: - consumer.setWechatId(consumerToken.getOpenid()); - consumer.setNickname(consumerToken.getNickname()); - setHeader(consumerToken, consumer, false); - setSex(consumerToken, consumer); - consumer.setRegType(DictRegTypeConsts.DICT_WECHAT); - break; - case LogTypeConsts.WEIBO: - consumer.setWeiboId(consumerToken.getOpenid()); - consumer.setNickname(consumerToken.getNickname()); - setHeader(consumerToken, consumer, false); - setSex(consumerToken, consumer); - consumer.setRegType(DictRegTypeConsts.DICT_WEIBO); - break; - default: - return new ApiVO(ApiVO.STATUS.F, "暂不支持的登录方式"); - } - Integer result = consumerMapper.insert(consumer); - if (result < 1) { - return new ApiVO(ApiVO.STATUS.F, RespsConsts.CODE_FAIL, "登录失败"); - } - return new ApiVO(ApiVO.STATUS.S).setObject1(consumer); - } - - public Consumer getConsumerByQQOpenid(String openid) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("qq_id", openid); diff --git a/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/web/SecurityCtrl.java b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/web/SecurityCtrl.java index 8f5ea6f..549faf9 100644 --- a/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/web/SecurityCtrl.java +++ b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/web/SecurityCtrl.java @@ -1,7 +1,23 @@ package com.yexuejc.springboot.base.web; +import com.yexuejc.base.http.Resps; +import com.yexuejc.base.pojo.ApiVO; +import com.yexuejc.base.util.RegexUtil; +import com.yexuejc.base.util.StrUtil; +import com.yexuejc.springboot.base.autoconfigure.MutiRedisAutoConfiguration; +import com.yexuejc.springboot.base.constant.BizConsts; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + /** *
  * Security 登录注册相关controller
@@ -19,5 +35,49 @@ import org.springframework.web.bind.annotation.RestController;
 @RestController
 public class SecurityCtrl {
 
+    @Autowired
+    @Qualifier(MutiRedisAutoConfiguration.BEAN_REDIS_TEMPLATE1)
+    RedisTemplate redisTemplate;
+
+    /**
+     * 登录发送短信
+     *
+     * @param mobile
+     * @return
+     */
+    @RequestMapping(value = "/login/{mobile}", method = RequestMethod.POST)
+    public Resps login(@PathVariable String mobile) {
+        if (!RegexUtil.regex(mobile, RegexUtil.REGEX_MOBILE)) {
+            return Resps.fail("手机号不正确");
+        }
+        ApiVO apiVO = sendSmsCode(BizConsts.CONSUMER_LOGIN_SMS, mobile);
+        if (apiVO.isFail()) {
+            return Resps.fail(apiVO.getMsg());
+        }
+        return Resps.success(apiVO.getMsg());
+    }
+
+    private ApiVO sendSmsCode(String smsType, String mobile) {
+        String smsId = StrUtil.genUUID(30);
+        String code = StrUtil.genNum().substring(2, 8);
+        //自己接入短信发送
+        boolean result = true;
+        if (result) {
+            //成功
+            //存reids
+            Map map = new HashMap<>();
+            map.put("smsId", smsId);
+            map.put("code", code);
+            map.put("trade_id", "短信返回id");
+            map.put("validatedNums", 0);
+            redisTemplate.afterPropertiesSet();
+            redisTemplate.opsForHash().putAll(smsType + "." + mobile, map);
+            // 过期时间:5分钟
+            redisTemplate.expire(smsType + "." + mobile, 5 * 60, TimeUnit.SECONDS);
+            return new ApiVO(ApiVO.STATUS.S, "短信发送成功");
+        } else {
+            return new ApiVO(ApiVO.STATUS.F, "短信发送失败");
+        }
+    }
 
 }