1.2.0多方登录稳定版

This commit is contained in:
yexuejc 2018-12-01 12:22:45 +08:00
parent 02d4a44efc
commit f61f905483
17 changed files with 253 additions and 79 deletions

View File

@ -13,9 +13,9 @@ parent版本封装<br/>
base:功能封装
#### 最新版本
* 1.x yexuejc.springboot.version=1.1.4 <br>
* 2.x yexuejc.springboot.version=2.0.3 <br>
* yexuejc.base.version=1.2.1
* 1.x yexuejc.springboot.version=1.2.0 <br>
* 2.x yexuejc.springboot.version=2.0.5 <br>
* yexuejc.base.version=1.2.4
pom.xml
```

View File

@ -1,6 +1,20 @@
yexuejc-springboot 更新内容
-------------------
#### version 1.2.0
**time2018-12-1 12:19:06** <br/>
**branch** master <br/>
**关联工程:** <br/>
```
springboot-base:1.2.4
spring-boot-starter-parent:1.5.16.RELEASE
```
**update** <br/>
1. security多方登录第一个稳定版<br/>
支持账号登录、短信登录、第三方授权openid登录<br/>
功能链接[security重构-多方登录](doc/SECURITY.md)
#
#### version 1.1.6-1.1.9
**time2018-11-21 15:03:01** <br/>
**branch** master <br/>

View File

@ -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框架封装集成登录 使用指南
</dependencies>
```
> **相关文件说明** 所有核心文件都在 com.yexuejc.springboot.base.security 包下
#### 现附上系统实现逻辑图
![多方登录系统实现逻辑图](多方登录设计.jpg)
1.com.yexuejc.springboot.base.security.SecurityConfig
<br/>
@ -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
<br/>
**获取登录用户信息**

24
doc/Securtity效果图.md Normal file
View File

@ -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)

BIN
doc/sl/sl_01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
doc/sl/sl_02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
doc/sl/sl_10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
doc/sl/sl_err.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
doc/sl/sl_gq.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
doc/sl/sl_ss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
doc/sl/sl_t3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
doc/sl/sl_t4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
doc/多方登录设计.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

View File

@ -5,7 +5,7 @@
<groupId>com.yexuejc.springboot</groupId>
<artifactId>yexuejc-springboot-parent</artifactId>
<version>1.1.9</version>
<version>1.2.0</version>
<packaging>pom</packaging>
<name>${project.artifactId}</name>
@ -20,7 +20,7 @@
</parent>
<properties>
<yexuejc.base.version>1.2.2</yexuejc.base.version>
<yexuejc.base.version>1.2.4</yexuejc.base.version>
<repos.yexuejc.url>https://nexus.yexuejc.club/repository/</repos.yexuejc.url>
<repos.aliyun.url>http://maven.aliyun.com/nexus/content/groups/public</repos.aliyun.url>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>com.yexuejc.springboot</groupId>
<artifactId>yexuejc-springboot-parent</artifactId>
<version>1.1.9</version>
<version>1.2.0</version>
<!-- 本地打包:使用相对关联路径 -->
<!--<relativePath>../../yexuejc</relativePath>-->
</parent>

View File

@ -13,11 +13,13 @@ 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;
import java.util.ArrayList;
@ -48,14 +50,37 @@ public class UserServiceImpl implements UserService {
* @return
*/
@Override
public User getConsumerByUserName(String username) {
public Object getConsumerByUserName(String username) {
if (StrUtil.isEmpty(username)) {
throw new UsernameNotFoundException("username为空一般是第三方登录来的直接抛出UsernameNotFoundException就是");
}
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("mobile", username);
Consumer consumer = consumerMapper.selectOne(queryWrapper);
ArrayList roles = new ArrayList<>();
if (null == consumer) {
/**
* 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<GrantedAuthority> 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;
}
/**
@ -88,15 +113,14 @@ public class UserServiceImpl implements UserService {
}
/**
* 第三方登录
* 校验openid 根据自己业务做判断
* <br/>
* 返回封装登录用户信息到 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:
@ -111,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 第三方授权异常}
* <br/>
*
* @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<String> 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登录
*
@ -173,69 +277,6 @@ public class UserServiceImpl implements UserService {
}
/**
* 没有账号时处理自己的业务此次必须返回 构造出的登录用户否则会抛出{@link ThirdPartyAuthorizationException 第三方授权异常}
* <br/>
* 返回封装登录用户信息到 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<String> 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);

View File

@ -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;
/**
* <pre>
* Security 登录注册相关controller
@ -19,5 +35,49 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
public class SecurityCtrl {
@Autowired
@Qualifier(MutiRedisAutoConfiguration.BEAN_REDIS_TEMPLATE1)
RedisTemplate<Object, Object> 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<String, Object> 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, "短信发送失败");
}
}
}