From 6eb91109ac11ed4fd0c67dba234bd80be69321c0 Mon Sep 17 00:00:00 2001 From: maxf <1107047387@qq.com> Date: Fri, 9 Nov 2018 15:29:42 +0800 Subject: [PATCH] =?UTF-8?q?springboot=20security=20=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=9B=86=E6=88=90=EF=BC=88=E5=90=AB=E8=B4=A6=E5=8F=B7=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E3=80=81=E7=9F=AD=E4=BF=A1=E7=99=BB=E5=BD=95=E3=80=81?= =?UTF-8?q?=E7=AC=AC=E4=B8=89=E6=96=B9=E7=99=BB=E5=BD=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yexuejc-springboot-base/pom.xml | 23 ++ .../base/autoconfigure/OssFacade.java | 2 +- .../springboot/base/constant/BizConsts.java | 21 ++ .../base/constant/DictRegTypeConsts.java | 34 ++ .../base/constant/LogTypeConsts.java | 31 ++ .../ThirdPartyAuthorizationException.java | 42 +++ ...onsumerAuthenticationProcessingFilter.java | 275 ++++++++++++++ .../ConsumerAuthenticationProvider.java | 290 +++++++++++++++ .../ConsumerSecurityContextRepository.java | 129 +++++++ .../base/security/ConsumerToken.java | 147 ++++++++ .../base/security/ConsumerUser.java | 90 +++++ .../springboot/base/security/LoginToken.java | 38 ++ .../base/security/SecurityConfig.java | 152 ++++++++ .../base/security/UserDetailsManager.java | 48 +++ .../springboot/base/security/inte/User.java | 32 ++ .../base/security/inte/UserService.java | 48 +++ .../springboot/base/ApplicationRun.java | 4 +- .../base/mapper/ConsumerMapper.java | 39 ++ .../base/mapper/handler/JsonTypeHandler.java | 53 +++ .../base/security/MySecurityConfig.java | 184 ++++++++++ .../base/security/UserServiceImpl.java | 337 ++++++++++++++++++ .../base/security/domain/Consumer.java | 281 +++++++++++++++ .../springboot/base/web/IndexCtrl.java | 1 + .../springboot/base/web/SecurityCtrl.java | 22 ++ .../src/test/resources/application.properties | 65 +++- .../src/test/resources/db/data.sql | 34 ++ .../src/test/resources/db/schema.sql | 42 +++ .../test/resources/mapper/ConsumerMapper.xml | 82 +++++ 28 files changed, 2534 insertions(+), 12 deletions(-) create mode 100644 yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/constant/DictRegTypeConsts.java create mode 100644 yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/constant/LogTypeConsts.java create mode 100644 yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/exception/ThirdPartyAuthorizationException.java create mode 100644 yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerAuthenticationProcessingFilter.java create mode 100644 yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerAuthenticationProvider.java create mode 100644 yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerSecurityContextRepository.java create mode 100644 yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerToken.java create mode 100644 yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerUser.java create mode 100644 yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/LoginToken.java create mode 100644 yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/SecurityConfig.java create mode 100644 yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/UserDetailsManager.java create mode 100644 yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/inte/User.java create mode 100644 yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/inte/UserService.java create mode 100644 yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/mapper/ConsumerMapper.java create mode 100644 yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/mapper/handler/JsonTypeHandler.java create mode 100644 yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/security/MySecurityConfig.java create mode 100644 yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/security/UserServiceImpl.java create mode 100644 yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/security/domain/Consumer.java create mode 100644 yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/web/SecurityCtrl.java create mode 100644 yexuejc-springboot-base/src/test/resources/db/data.sql create mode 100644 yexuejc-springboot-base/src/test/resources/db/schema.sql create mode 100644 yexuejc-springboot-base/src/test/resources/mapper/ConsumerMapper.xml diff --git a/yexuejc-springboot-base/pom.xml b/yexuejc-springboot-base/pom.xml index 4f482f0..67e1525 100644 --- a/yexuejc-springboot-base/pom.xml +++ b/yexuejc-springboot-base/pom.xml @@ -92,6 +92,29 @@ true test + + + + com.zaxxer + HikariCP + true + test + + + + com.baomidou + mybatis-plus-boot-starter + true + test + + + + com.h2database + h2 + true + test + + diff --git a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/autoconfigure/OssFacade.java b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/autoconfigure/OssFacade.java index df15828..a17ef69 100644 --- a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/autoconfigure/OssFacade.java +++ b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/autoconfigure/OssFacade.java @@ -68,7 +68,7 @@ public class OssFacade { } private OSSClient ossClient() { - return new OSSClient(properties.getEndpoint(), properties.getAccessKeyID(), properties.getAccessKeySecret(), + return new OSSClient(properties.getEndpoint(), properties.getAccessKeyId(), properties.getAccessKeySecret(), configuration); } } diff --git a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/constant/BizConsts.java b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/constant/BizConsts.java index 1797355..dd401d7 100644 --- a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/constant/BizConsts.java +++ b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/constant/BizConsts.java @@ -69,4 +69,25 @@ public class BizConsts { */ public static String BASE_NOT_LOGIN_CODE = BASE_CODE + "010"; public static String BASE_NOT_LOGIN_MSG = "您尚未登陆"; + + + + + /** + * 用户登录发送短信验证码 + */ + public static String CONSUMER_LOGIN_SMS = "consumer.login.sendSms"; + /** + * 用户绑定手机号 + */ + public static String CONSUMER_BIND_MOBILE = "consumer.bind.mobile.sms"; + /** + * 用户登录信息 + */ + public static String CONSUMER_LOGIN_REDIS = "consumer.login.redis.session"; + + /** + * 后台管理员登录信息 + */ + public static String ADMIN_LOGIN_REDIS = "admin.login.redis.session"; } diff --git a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/constant/DictRegTypeConsts.java b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/constant/DictRegTypeConsts.java new file mode 100644 index 0000000..b96de86 --- /dev/null +++ b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/constant/DictRegTypeConsts.java @@ -0,0 +1,34 @@ +package com.yexuejc.springboot.base.constant; + +/** + * 注册类型 + * @author: maxf + * @date: 2018/8/6 21:43:58 + */ +public class DictRegTypeConsts { + /** + * A : 账号注册 + */ + public static final String DICT_ACCOUNT = "A"; + /** + * B : 微博注册 + */ + public static final String DICT_WEIBO = "B"; + /** + * Q : qq注册 + */ + public static final String DICT_QQ = "Q"; + /** + * S : 手机号注册 + */ + public static final String DICT_MOBILE = "S"; + /** + * W : 微信注册 + */ + public static final String DICT_WECHAT = "W"; + + private DictRegTypeConsts() { + } + + +} diff --git a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/constant/LogTypeConsts.java b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/constant/LogTypeConsts.java new file mode 100644 index 0000000..1a8cfea --- /dev/null +++ b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/constant/LogTypeConsts.java @@ -0,0 +1,31 @@ +package com.yexuejc.springboot.base.constant; + +/** + * 登录方式 + * + * @author: maxf + * @date: 2018/9/9 12:22:53 + */ +public class LogTypeConsts { + + /** + * 短信登录 + */ + public static final String SMS = "sms"; + /** + * QQ 登录 + */ + public static final String QQ = "qq"; + /** + * 微信 + */ + public static final String WECHAT = "wechat"; + /** + * 微博 + */ + public static final String WEIBO = "weibo"; + /** + * 账号 + */ + public static final String ACCOUNT = "account"; +} diff --git a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/exception/ThirdPartyAuthorizationException.java b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/exception/ThirdPartyAuthorizationException.java new file mode 100644 index 0000000..6cf3f17 --- /dev/null +++ b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/exception/ThirdPartyAuthorizationException.java @@ -0,0 +1,42 @@ +package com.yexuejc.springboot.base.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * 第三方授权异常 + * + * @author: maxf + * @date: 2018/5/27 19:20 + */ +public class ThirdPartyAuthorizationException extends AuthenticationException { + /** + * 错误码 + */ + private String code = "E"; + + public ThirdPartyAuthorizationException(String msg, Throwable t) { + super(msg, t); + } + + public ThirdPartyAuthorizationException(String msg) { + super(msg); + } + + public ThirdPartyAuthorizationException() { + super("授权异常"); + } + + public ThirdPartyAuthorizationException(Throwable t) { + super("授权异常", t); + } + + + public ThirdPartyAuthorizationException(String code, String message) { + super(message); + this.code = code; + } + public ThirdPartyAuthorizationException(String code, String msg, Throwable t) { + super(msg, t); + this.code = code; + } +} diff --git a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerAuthenticationProcessingFilter.java b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerAuthenticationProcessingFilter.java new file mode 100644 index 0000000..55367f1 --- /dev/null +++ b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerAuthenticationProcessingFilter.java @@ -0,0 +1,275 @@ +package com.yexuejc.springboot.base.security; + +import com.yexuejc.springboot.base.constant.LogTypeConsts; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Collections; + +public class ConsumerAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { + // ~ Static fields/initializers + // ===================================================================================== + + public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; + public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; + /** + * 登录方式 + */ + public static final String SPRING_SECURITY_FORM_LOGTYPE_KEY = "logtype"; + public static final String SPRING_SECURITY_FORM_OPENID_KEY = "openid"; + /********************************** 第三方登录时附带信息*************************************/ + /** + * 头像 + */ + public static final String SPRING_SECURITY_FORM_HEAD_KEY = "head"; + /** + * 昵称 + */ + public static final String SPRING_SECURITY_FORM_NICKNAME_KEY = "nickname"; + /** + * 性别 + */ + public static final String SPRING_SECURITY_FORM_SEX_KEY = "sex"; + /********************************** 第三方登录时附带信息*************************************/ + + private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY; + private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY; + private String logtypeParameter = SPRING_SECURITY_FORM_LOGTYPE_KEY; + private String openidParameter = SPRING_SECURITY_FORM_OPENID_KEY; + private String headParameter = SPRING_SECURITY_FORM_HEAD_KEY; + private String nicknameParameter = SPRING_SECURITY_FORM_NICKNAME_KEY; + private String sexParameter = SPRING_SECURITY_FORM_SEX_KEY; + private boolean postOnly = true; + private boolean reverse = true; + + // ~ Constructors + // =================================================================================================== + + protected ConsumerAuthenticationProcessingFilter(AuthenticationManager authenticationManager) { + super(new AntPathRequestMatcher("/login", "POST")); + setAuthenticationManager(authenticationManager); + } + + protected ConsumerAuthenticationProcessingFilter() { + super(new AntPathRequestMatcher("/login", "POST")); + } + // ~ Methods + // ======================================================================================================== + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, + HttpServletResponse response) throws AuthenticationException { + return displyAuthentication(request, response); + } + + /** + * 可继承自定义处理登录请求 + * + * @param request + * @param response + * @return + * @throws AuthenticationException + */ + protected Authentication displyAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + if (postOnly && !request.getMethod().equals("POST")) { + throw new AuthenticationServiceException( + "Authentication method not supported: " + request.getMethod()); + } + String logtype = obtainLogtype(request); + System.out.println("登录方式:" + logtype); + String username = ""; + String password = ""; + if (logtype == null) { + logtype = ""; + } + String openid = ""; + String smscode = ""; + /**第三方登录:微信 用户头像*/ + String head = ""; + String nickname = ""; + String sex = ""; + //根据不同登录方式做不同处理 + getParams(request, logtype, username, password, smscode, openid, sex, head, nickname); + UsernamePasswordAuthenticationToken authRequest = new ConsumerToken( + logtype, smscode, openid, username, password, head, nickname, sex); + + // Allow subclasses to set the "details" property + setDetails(request, authRequest); + //ProviderManager 这东西会先把DaoAuthenticationProvider加入列表,导致setHideUserNotFoundExceptions(false);被覆盖 + if (this.getAuthenticationManager() instanceof ProviderManager && reverse) { + reverse = false; + ProviderManager providerManager = (ProviderManager) this.getAuthenticationManager(); + providerManager.getProviders().forEach(it -> { + ((AbstractUserDetailsAuthenticationProvider) it).setHideUserNotFoundExceptions(false); + }); + Collections.reverse(providerManager.getProviders()); + } + return this.getAuthenticationManager().authenticate(authRequest); + } + + /** + * 根据登录方式获取请求参数 + * + * @param request 登录请求 + * @param logtype 登录类型 + * @param username 账号 + * @param password 密码 + * @param smscode 短信验证码 + * @param openid 第三封授权id + * @param sex 附加:性别 + * @param head 附加:头像(源头像路径) + * @param nickname 附加:昵称 + */ + protected void getParams(HttpServletRequest request, String logtype, String username, String password, + String smscode, String openid, String sex, String head, String nickname) { + switch (logtype) { + case LogTypeConsts.SMS: + //短信登录 + username = obtainUsername(request); + smscode = obtainPassword(request); + break; + case LogTypeConsts.QQ: + //QQ登录 + openid = obtainOpenid(request); + head = obtainHead(request); + nickname = obtainNickname(request); + sex = obtainSex(request); + break; + case LogTypeConsts.WECHAT: + //微信登录 + openid = obtainOpenid(request); + head = obtainHead(request); + nickname = obtainNickname(request); + sex = obtainSex(request); + break; + case LogTypeConsts.WEIBO: + //微博登录 + openid = obtainOpenid(request); + head = obtainHead(request); + nickname = obtainNickname(request); + sex = obtainSex(request); + break; + default: + //默认账号+密码登录 + username = obtainUsername(request).trim(); + password = obtainPassword(request); + break; + } + } + + /** + * Enables subclasses to override the composition of the password, such as by + * including additional values and a separator. + *

+ * This might be used for example if a postcode/zipcode was required in addition to + * the password. A delimiter such as a pipe (|) should be used to separate the + * password and extended value(s). The AuthenticationDao will need to + * generate the expected password in a corresponding manner. + *

+ * + * @param request so that request attributes can be retrieved + * @return the password that will be presented in the Authentication + * request token to the AuthenticationManager + */ + protected String obtainPassword(HttpServletRequest request) { + return request.getParameter(passwordParameter); + } + + /** + * Enables subclasses to override the composition of the username, such as by + * including additional values and a separator. + * + * @param request so that request attributes can be retrieved + * @return the username that will be presented in the Authentication + * request token to the AuthenticationManager + */ + protected String obtainUsername(HttpServletRequest request) { + return request.getParameter(usernameParameter); + } + + protected String obtainLogtype(HttpServletRequest request) { + return request.getParameter(logtypeParameter); + } + + protected String obtainOpenid(HttpServletRequest request) { + return request.getParameter(openidParameter); + } + + protected String obtainHead(HttpServletRequest request) { + return request.getParameter(headParameter); + } + + protected String obtainNickname(HttpServletRequest request) { + return request.getParameter(nicknameParameter); + } + + protected String obtainSex(HttpServletRequest request) { + return request.getParameter(sexParameter); + } + + /** + * Provided so that subclasses may configure what is put into the authentication + * request's details property. + * + * @param request that an authentication request is being created for + * @param authRequest the authentication request object that should have its details + * set + */ + protected void setDetails(HttpServletRequest request, + UsernamePasswordAuthenticationToken authRequest) { + authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); + } + + /** + * Sets the parameter name which will be used to obtain the username from the login + * request. + * + * @param usernameParameter the parameter name. Defaults to "username". + */ + public void setUsernameParameter(String usernameParameter) { + Assert.hasText(usernameParameter, "Username parameter must not be empty or null"); + this.usernameParameter = usernameParameter; + } + + /** + * Sets the parameter name which will be used to obtain the password from the login + * request.. + * + * @param passwordParameter the parameter name. Defaults to "password". + */ + public void setPasswordParameter(String passwordParameter) { + Assert.hasText(passwordParameter, "Password parameter must not be empty or null"); + this.passwordParameter = passwordParameter; + } + + /** + * Defines whether only HTTP POST requests will be allowed by this filter. If set to + * true, and an authentication request is received which is not a POST request, an + * exception will be raised immediately and authentication will not be attempted. The + * unsuccessfulAuthentication() method will be called as if handling a failed + * authentication. + *

+ * Defaults to true but may be overridden by subclasses. + */ + public void setPostOnly(boolean postOnly) { + this.postOnly = postOnly; + } + + public final String getUsernameParameter() { + return usernameParameter; + } + + public final String getPasswordParameter() { + return passwordParameter; + } +} diff --git a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerAuthenticationProvider.java b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerAuthenticationProvider.java new file mode 100644 index 0000000..09494a1 --- /dev/null +++ b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerAuthenticationProvider.java @@ -0,0 +1,290 @@ +package com.yexuejc.springboot.base.security; + +import com.yexuejc.base.pojo.ApiVO; +import com.yexuejc.base.util.StrUtil; +import com.yexuejc.springboot.base.constant.BizConsts; +import com.yexuejc.springboot.base.constant.LogTypeConsts; +import com.yexuejc.springboot.base.exception.ThirdPartyAuthorizationException; +import com.yexuejc.springboot.base.security.inte.User; +import com.yexuejc.springboot.base.security.inte.UserService; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.*; +import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + *

主要做验证用户使用

+ *
+ *     密码登录:校验账号密码
+ *     短信登录:校验短信验证码
+ *     第三方登录:校验openid=>用户中心(数据库)不存在,进行新增数据操作
+ * 
+ * + * @author maxf + * @version 1.0 + * @ClassName ConsumerAuthenticationProvider + * @Description + * @date 2018/11/9 11:23 + */ +public class ConsumerAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { + // ~ Static fields/initializers + // ===================================================================================== + + /** + * The plaintext password used to perform + * PasswordEncoder#matches(CharSequence, String)} on when the user is + * not found to avoid SEC-2056. + */ + private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword"; + + // ~ Instance fields + // ================================================================================================ + + private PasswordEncoder passwordEncoder; + + /** + * The password used to perform + * {@link PasswordEncoder#matches(CharSequence, String)} on when the user is + * not found to avoid SEC-2056. This is necessary, because some + * {@link PasswordEncoder} implementations will short circuit if the password is not + * in a valid format. + */ + private volatile String userNotFoundEncodedPassword; + + private UserDetailsService userDetailsService; + private final UserService accountView; + + + public ConsumerAuthenticationProvider(UserDetailsService userDetailsService, UserService accountView) { +// super(); + super.setHideUserNotFoundExceptions(false); +// // 这个地方一定要对userDetailsService赋值,不然userDetailsService是null (这个坑有点深) + setUserDetailsService(userDetailsService); + setPasswordEncoder(createDelegatingPasswordEncoder()); + this.accountView = accountView; + } + + /** + * 定义加密方式 + * 默认MD5加密 + * + * @return + */ + protected PasswordEncoder createDelegatingPasswordEncoder() { + String encodingId = "MD5"; + Map encoders = new HashMap<>(9); + encoders.put("bcrypt", new BCryptPasswordEncoder()); + encoders.put("ldap", new LdapShaPasswordEncoder()); + encoders.put("MD4", new Md4PasswordEncoder()); + encoders.put("MD5", new MessageDigestPasswordEncoder("MD5")); + encoders.put("noop", NoOpPasswordEncoder.getInstance()); + encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); + encoders.put("scrypt", new SCryptPasswordEncoder()); + encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1")); + encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256")); + encoders.put("sha256", new StandardPasswordEncoder()); + return new DelegatingPasswordEncoder(encodingId, encoders); + } + + @Override + protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { + String requestPwd = authentication.getCredentials().toString(); + if (authentication instanceof ConsumerToken) { + ConsumerToken consumerToken = (ConsumerToken) authentication; + + + displyAuthenticationChecks(consumerToken, requestPwd, userDetails.getPassword()); + } + } + + /** + * 登录密码校验 + * + * @param consumerToken 登录信息 + * @param requestPwd 登录密码 + * @param dbPwd 源用户密码(数据库中的密码) + */ + protected void displyAuthenticationChecks(ConsumerToken consumerToken, String requestPwd, String dbPwd) { + ApiVO apiVO; + switch (consumerToken.getLogtype()) { + case LogTypeConsts.SMS: + //新注册账号已经在注册的时候校验的短信验证码,所有这里不校验 + if (!consumerToken.isReg) { + apiVO = accountView.checkSmsCode2Redis(BizConsts.CONSUMER_LOGIN_SMS, consumerToken.getUsername(), consumerToken.getSmscode()); + if (apiVO.isFail()) { + throw new ThirdPartyAuthorizationException(apiVO.getMsg()); + } + } + break; + case LogTypeConsts.QQ: + break; + case LogTypeConsts.WECHAT: + + break; + case LogTypeConsts.WEIBO: + break; + default: + //账号登录 + if (!passwordEncoder.matches(requestPwd, "{MD5}" + dbPwd)) { + logger.debug("Authentication failed: password does not match stored value"); + + throw new ThirdPartyAuthorizationException("密码错误"); + } + break; + } + } + + @Override + protected void doAfterPropertiesSet() throws Exception { + Assert.notNull(this.userDetailsService, "A UserDetailsService must be set"); + } + + @Override + protected final UserDetails retrieveUser(String username, + UsernamePasswordAuthenticationToken authentication) + throws AuthenticationException { + UserDetails loadedUser = null; + + String logtype = ""; + ConsumerToken consumerToken = null; + if (authentication instanceof ConsumerToken) { + consumerToken = (ConsumerToken) authentication; + logtype = StrUtil.setStr(consumerToken.getLogtype(), ""); + } + try { + //通过username查询数据库,当第三方登录时所传参数为openid(没有传username),此次会抛出找DB不到账号信息的异常,交给catch处理 + prepareTimingAttackProtection(); + loadedUser = this.getUserDetailsService().loadUserByUsername(username); + } catch (UsernameNotFoundException notFound) { + //账号登录 + if ((StrUtil.isEmpty(logtype) || LogTypeConsts.ACCOUNT.equals(logtype))) { + mitigateAgainstTimingAttack(authentication); + throw notFound; + } else { + try { + //其他方式登录:查询账号 没有->创建账号 + //第三方登录 + if (consumerToken != null && StrUtil.isNotEmpty(consumerToken.getOpenid())) { + ApiVO apiVO = accountView.checkOpenId(consumerToken); + if (apiVO.isSucc()) { + //已有账号 + User consumer = apiVO.getObject1(User.class); + // 处理用户权限 + List authorities = new ArrayList<>(); + for (String role : consumer.getRoles()) { + authorities.add(new SimpleGrantedAuthority(role)); + } + loadedUser = new ConsumerUser( + StrUtil.isEmpty(consumer.getMobile()) ? consumerToken.getOpenid() : consumer.getMobile(), + consumer.getPwd(), consumer.getEnable(), consumer.getNonExpire(), + true, consumer.getNonLock(), authorities, consumer.getConsumerId(), + logtype, System.currentTimeMillis()); + return loadedUser; + } + } + //第三方登录+短信登录 + if (consumerToken != null) { + //没有->创建账号 + consumerToken.isReg = true; + ApiVO apiVO = accountView.addConsumer(consumerToken); + if (apiVO.isSucc()) { + loadedUser = display(consumerToken, apiVO.getObject1(User.class)); + return loadedUser; + } else { + throw new ThirdPartyAuthorizationException(apiVO.getMsg()); + } + } + } catch (Exception e) { + e.printStackTrace(); + if (e instanceof ThirdPartyAuthorizationException) { + throw e; + } + throw new ThirdPartyAuthorizationException("登录失败,请稍后重试"); + } + throw notFound; + } + } catch (Exception repositoryProblem) { + throw new InternalAuthenticationServiceException( + repositoryProblem.getMessage(), repositoryProblem); + } + if (loadedUser == null) { + throw new InternalAuthenticationServiceException( + "UserDetailsService returned null, which is an interface contract violation"); + } + return loadedUser; + } + + private void prepareTimingAttackProtection() { + if (this.userNotFoundEncodedPassword == null) { + this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD); + } + } + + private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) { + if (authentication.getCredentials() != null) { + String presentedPassword = authentication.getCredentials().toString(); + this.passwordEncoder.matches(presentedPassword, "{MD5}" + this.userNotFoundEncodedPassword); + } + } + + /** + * 处理用户为登录用户 + * + * @param consumerToken 登录用户信息 + * @param consumer 实际用户信息 + * @return response User + */ + private UserDetails display(ConsumerToken consumerToken, User consumer) { + // 处理用户权限 + List authorities = new ArrayList<>(); + for (String role : consumer.getRoles()) { + authorities.add(new SimpleGrantedAuthority(role)); + } + UserDetails userDetails = new ConsumerUser( + StrUtil.isEmpty(consumer.getMobile()) ? consumerToken.getOpenid() : consumer.getMobile(), + consumer.getPwd(), consumer.getEnable(), consumer.getNonExpire(), + true, consumer.getNonLock(), authorities, consumer.getConsumerId(), + consumerToken.getLogtype(), System.currentTimeMillis()); + return userDetails; + } + + /** + * Sets the PasswordEncoder instance to be used to encode and validate passwords. If + * not set, the password will be compared using {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()} + * + * @param passwordEncoder must be an instance of one of the {@code PasswordEncoder} + * types. + */ + public void setPasswordEncoder(PasswordEncoder passwordEncoder) { + Assert.notNull(passwordEncoder, "passwordEncoder cannot be null"); + this.passwordEncoder = passwordEncoder; + this.userNotFoundEncodedPassword = null; + } + + protected PasswordEncoder getPasswordEncoder() { + return passwordEncoder; + } + + public void setUserDetailsService(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + protected UserDetailsService getUserDetailsService() { + return userDetailsService; + } +} \ No newline at end of file diff --git a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerSecurityContextRepository.java b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerSecurityContextRepository.java new file mode 100644 index 0000000..43d8782 --- /dev/null +++ b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerSecurityContextRepository.java @@ -0,0 +1,129 @@ +package com.yexuejc.springboot.base.security; + +import com.yexuejc.base.constant.RespsConsts; +import com.yexuejc.base.util.JwtUtil; +import com.yexuejc.springboot.base.constant.BizConsts; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.web.context.HttpRequestResponseHolder; +import org.springframework.security.web.context.SecurityContextRepository; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Security 上下文配置 + *

+ * 校验token=>返回token携带用户(登录用户)信息 + *

+ * + * @ClassName: ConsumerSecurityContextRepository + * @Description:Security 上下文配置 + * @author: maxf + * @date: 2017年11月22日 下午4:39:20 + */ +public class ConsumerSecurityContextRepository implements SecurityContextRepository { + protected final Log logger = LogFactory.getLog(this.getClass()); + private static final String TOKEN = "token"; + private static final String ROLES = "roles"; + + private final RedisTemplate redisTemplate0; + + public ConsumerSecurityContextRepository(RedisTemplate redisTemplate0) { + this.redisTemplate0 = redisTemplate0; + } + + @Override + public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { + SecurityContext context = readSecurityContext(requestResponseHolder.getRequest()); + if (context == null) { + if (logger.isDebugEnabled()) { + logger.debug("No SecurityContext was available, A new one will be created."); + } + context = SecurityContextHolder.createEmptyContext(); + } + return context; + } + + /** + * Stores the security context on completion of a request. + *

Title: saveContext

+ *

Description:Stores the security context on completion of a request.

+ * + * @param context + * @param request + * @param response + * @see SecurityContextRepository#saveContext(SecurityContext, HttpServletRequest, HttpServletResponse) + */ + @Override + public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { + + } + + @Override + public boolean containsContext(HttpServletRequest request) { + return readSecurityContext(request) != null; + } + + /** + *
+     * 过滤登录认证
+     * 默认:header=>Authorization:token
+     * token 为 jwt串
+     * 此处使用登录返回的token解析到具体的登录用户,返回登录用户信息;如果没有,者无权限请求该接口
+     * 
+ * + * @param request + */ + protected SecurityContext readSecurityContext(HttpServletRequest request) { + final boolean debug = logger.isDebugEnabled(); + + String token = request.getHeader(RespsConsts.HEADER_AUTHORIZATION); + if (token == null || token.length() == 0) { + return null; + } + LoginToken consumerToken = JwtUtil.instace().parse(token, LoginToken.class); + if (consumerToken == null || consumerToken.getUsername() == null || consumerToken.getUsername().length() == 0) { + return null; + } + // 根据token中携带的username查询用户信息 + Map entry = redisTemplate0.opsForHash() + .entries(BizConsts.CONSUMER_LOGIN_REDIS + "." + consumerToken.getUsername()); + if (entry == null) { + return null; + } + if (!token.equals(entry.get(TOKEN))) { + return null; + } + // 处理用户权限 + List authorities = new ArrayList<>(); + for (String role : (List) entry.get(ROLES)) { + authorities.add(new SimpleGrantedAuthority(role)); + } + ConsumerUser consumerUser = new ConsumerUser((String) entry.get("username"), "", + true, true, true, true, authorities, + (String) entry.get("id"), (String) entry.get("logType"), (Long) entry.get("logTime")); + + SecurityContext context = new SecurityContextImpl(); + + consumerUser.eraseCredentials(); + context.setAuthentication( + new UsernamePasswordAuthenticationToken(consumerUser, null, consumerUser.getAuthorities())); + + if (debug) { + logger.debug("Obtained a valid SecurityContext : '" + context + "'"); + } + return context; + } + +} diff --git a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerToken.java b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerToken.java new file mode 100644 index 0000000..0f43a18 --- /dev/null +++ b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerToken.java @@ -0,0 +1,147 @@ +package com.yexuejc.springboot.base.security; + +import com.yexuejc.base.util.JsonUtil; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +/** + * 登录参数 request
+ * 用于存放授权后,需要提取的用户信息 + * + * @ClassName: ConsumerToken + * @Description: 用于存放授权后,需要提取的用户信息 + * @author: maxf + * @date: 2017年11月22日 下午4:39:29 + */ +public class ConsumerToken extends UsernamePasswordAuthenticationToken { + private static final long serialVersionUID = -1923797941L; + + + /** + * 消费者用户名(手机号) + */ + private String username; + /** + * 登录方式 + */ + private String logtype; + /** + * 短信验证码 + */ + private String smscode; + /** + * openid + */ + private String openid; + /********************************** 第三方登录时附带信息*************************************/ + /** + * 头像 + */ + private String head; + /** + * 昵称 + */ + private String nickname; + /** + * 性别 + */ + private String sex; + + + /********************************** 第三方登录时附带信息*************************************/ + + /** + * 是否注册账号:减少sms注册时短信校验次数 + */ + public boolean isReg = false; + + + public ConsumerToken(String username) { + super(null, null); + this.username = username; + } + + public ConsumerToken(Object principal, Object credentials) { + super(principal, credentials); + } + + public ConsumerToken(Object principal, Object credentials, Collection authorities) { + super(principal, credentials, authorities); + } + + public ConsumerToken(String logtype, String smscode, String openid, String username, Object credentials, String head, String nickname, + String sex) { + super(username, credentials); + this.username = username; + this.logtype = logtype; + this.smscode = smscode; + this.openid = openid; + this.head = head; + this.nickname = nickname; + this.sex = sex; + } + + + @Override + public String toString() { + return JsonUtil.obj2Json(this); + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getLogtype() { + return logtype; + } + + public void setLogtype(String logtype) { + this.logtype = logtype; + } + + public String getSmscode() { + return smscode; + } + + public void setSmscode(String smscode) { + this.smscode = smscode; + } + + public String getOpenid() { + return openid; + } + + public void setOpenid(String openid) { + this.openid = openid; + } + + public String getHead() { + return head; + } + + public void setHead(String head) { + this.head = head; + } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public String getSex() { + return sex; + } + + public void setSex(String sex) { + this.sex = sex; + } +} diff --git a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerUser.java b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerUser.java new file mode 100644 index 0000000..bdd16b0 --- /dev/null +++ b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/ConsumerUser.java @@ -0,0 +1,90 @@ +package com.yexuejc.springboot.base.security; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import java.util.Collection; + +/** + * 登录用户信息 response + * + * @ClassName: ConsumerUser + * @Description:登录用户信息 + * @author: maxf + * @date: 2017年11月22日 下午4:39:45 + */ +public class ConsumerUser extends User { + private static final long serialVersionUID = 1L; + + /** + * 消费者用户主键ID + */ + private String id; + /** + * 登录方式 + */ + private String logType; + /** + * 登录时间 + */ + private Long logTime; + + public ConsumerUser(String username, String password, boolean enabled, boolean accountNonExpired, + boolean credentialsNonExpired, boolean accountNonLocked, + Collection authorities, + String id, String logType, Long logTime) { + super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); + this.id = id; + this.logType = logType; + this.logTime = logTime; + } + + /** + * 仅做保存登录用户信息时使用 + * + * @param username + * @param id + * @param logType + * @param logTime + */ + public ConsumerUser(String username, String id, String logType, Long logTime) { + super(username, "", true, true, true, true, null); + this.id = id; + this.logType = logType; + this.logTime = logTime; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(super.toString()).append(": "); + + sb.append("id: ").append(this.id).append("; "); + + return sb.toString(); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getLogType() { + return logType; + } + + public void setLogType(String logType) { + this.logType = logType; + } + + public Long getLogTime() { + return logTime; + } + + public void setLogTime(Long logTime) { + this.logTime = logTime; + } +} diff --git a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/LoginToken.java b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/LoginToken.java new file mode 100644 index 0000000..4747153 --- /dev/null +++ b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/LoginToken.java @@ -0,0 +1,38 @@ +package com.yexuejc.springboot.base.security; + +import com.yexuejc.base.util.JsonUtil; + +import java.io.Serializable; + +/** + * 封装登录信息至JWT得到token + * + * @author: maxf + * @date: 2018/5/31 21:34 + */ +public class LoginToken implements Serializable { + /** + * 消费者用户名(手机号) + */ + private String username; + + public LoginToken() { + } + + @Override + public String toString() { + return JsonUtil.obj2Json(this); + } + + public LoginToken(String username) { + this.username = username; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } +} diff --git a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/SecurityConfig.java b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/SecurityConfig.java new file mode 100644 index 0000000..4b2c4d8 --- /dev/null +++ b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/SecurityConfig.java @@ -0,0 +1,152 @@ +package com.yexuejc.springboot.base.security; + +import com.yexuejc.springboot.base.autoconfigure.MutiRedisAutoConfiguration; +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.context.annotation.Bean; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.BeanIds; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; + +/** + * Security Web 配置 + * + * @ClassName: SecurityConfig + * @Description:Security Web 配置 + * @author: maxf + * @date: 2017年11月22日 下午4:40:22 + */ +//@EnableWebSecurity(debug = false) +public abstract class SecurityConfig extends WebSecurityConfigurerAdapter { + @Bean(name = BeanIds.AUTHENTICATION_MANAGER) + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Autowired + @Qualifier(MutiRedisAutoConfiguration.BEAN_REDIS_TEMPLATE0) + private RedisTemplate redisTemplate0; + + + @Bean + public static NoOpPasswordEncoder passwordEncoder() { + return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance(); + } + + /** + * 查询数据库DB=>获取用户数据loadUserByUsername() + * + * @return + */ + protected abstract UserService getUserService(); + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + UserDetailsService userDetailsService = new UserDetailsManager(getUserService()); + auth.authenticationProvider(new ConsumerAuthenticationProvider(userDetailsService, getUserService())); + auth.userDetailsService(userDetailsService); + } + + + @Bean + public ConsumerAuthenticationProcessingFilter consumerAuthenticationProcessingFilter( + AuthenticationManager authenticationManager) throws Exception { + ConsumerAuthenticationProcessingFilter filter = new ConsumerAuthenticationProcessingFilter + (authenticationManager); + filter.setAuthenticationManager(this.authenticationManager()); + loginHodler(filter); + return filter; + } + + /** + *
+     * 处理登录
+     * 成功: filter.setAuthenticationSuccessHandler()
+     * 失败: filter.setAuthenticationFailureHandler()
+     * 
+ * + * @param filter + */ + protected abstract void loginHodler(ConsumerAuthenticationProcessingFilter filter); + + + @Bean + public LoginUrlAuthenticationEntryPoint loginUrlAuthenticationEntryPoint() { + LoginUrlAuthenticationEntryPoint loginUrlAuthenticationEntryPoint = new LoginUrlAuthenticationEntryPoint + ("/login"); + return loginUrlAuthenticationEntryPoint; + } + + /** + * 解决跨域问题 + *
+     *      http.csrf().disable()
+     *          .cors()
+     *          .and()
+     * 
+ * 参考 https://www.jianshu.com/p/87e1ef68794c -> https://github.com/gothinkster/spring-boot-realworld-example-app + * + * @return + */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + final CorsConfiguration configuration = new CorsConfiguration(); + //指定允许跨域的请求(*所有):http://wap.guansichou.com + configuration.setAllowedOrigins(Arrays.asList("*")); + configuration.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH")); + // setAllowCredentials(true) is important, otherwise: + // The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode + // is 'include'. + configuration.setAllowCredentials(true); + // setAllowedHeaders is important! Without it, OPTIONS preflight request + // will fail with 403 Invalid CORS request + configuration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "X-User-Agent", "Content-Type")); + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + /** + * 关键.cors() + * + * @param http + * @throws Exception + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable() + .cors() + .and().servletApi().disable() + .requestCache().disable() + .securityContext().securityContextRepository(new ConsumerSecurityContextRepository(redisTemplate0)) + .and() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + http.addFilterAt(consumerAuthenticationProcessingFilter(super.authenticationManager()), + UsernamePasswordAuthenticationFilter.class); + } + + + @Override + public void configure(WebSecurity web) throws Exception { + // 不需要经过SpringSecurity的过滤器的URLs + // web.ignoring(); + } + +} diff --git a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/UserDetailsManager.java b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/UserDetailsManager.java new file mode 100644 index 0000000..5fb04aa --- /dev/null +++ b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/UserDetailsManager.java @@ -0,0 +1,48 @@ +package com.yexuejc.springboot.base.security; + +import com.yexuejc.base.util.StrUtil; +import com.yexuejc.springboot.base.security.inte.User; +import com.yexuejc.springboot.base.security.inte.UserService; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * ConsumerJdbcDaoImpl + * + * @ClassName: ConsumerJdbcDaoImpl + * @Description:ConsumerJdbcDaoImpl + * @author: maxf + * @date: 2017年12月21日 下午6:17:12 + */ +public class UserDetailsManager extends InMemoryUserDetailsManager { + + private final UserService userService; + + public UserDetailsManager(UserService userService) { + this.userService = userService; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User consumer = userService.getConsumerByUserName(username); + if (StrUtil.isEmpty(consumer)) { + throw new UsernameNotFoundException(username); + } + // 处理用户权限 + 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; + } + +} diff --git a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/inte/User.java b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/inte/User.java new file mode 100644 index 0000000..c0aba9a --- /dev/null +++ b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/inte/User.java @@ -0,0 +1,32 @@ +package com.yexuejc.springboot.base.security.inte; + +import java.util.List; + +/** + * @author maxf + * @version 1.0 + * @ClassName User + * @Description + * @date 2018/11/8 17:19 + */ +public interface User { + /** + * 角色 + * + * @return + */ + List getRoles(); + + + String getMobile(); + + String getPwd(); + + boolean getEnable(); + + boolean getNonExpire(); + + boolean getNonLock(); + + String getConsumerId(); +} diff --git a/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/inte/UserService.java b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/inte/UserService.java new file mode 100644 index 0000000..599f78f --- /dev/null +++ b/yexuejc-springboot-base/src/main/java/com/yexuejc/springboot/base/security/inte/UserService.java @@ -0,0 +1,48 @@ +package com.yexuejc.springboot.base.security.inte; + +import com.yexuejc.base.pojo.ApiVO; +import com.yexuejc.springboot.base.constant.BizConsts; +import com.yexuejc.springboot.base.security.ConsumerToken; + +/** + * @author maxf + * @version 1.0 + * @ClassName UserService + * @Description + * @date 2018/11/8 17:17 + */ +public interface UserService { + /** + * 根据用户名到数据库查询用户 + * + * @param username 登录账号 + * @return + */ + User getConsumerByUserName(String username); + + /** + * 校验短信验证码=>短信登录 + * + * @param redisBiz {@link BizConsts.CONSUMER_LOGIN_SMS} 业务id reids使用 + * @param username 登录账号 + * @param smscode 短信验证码 + * @return + */ + ApiVO checkSmsCode2Redis(String redisBiz, String username, String smscode); + + /** + * 校验第三方登录openid + * + * @param consumerToken 登录信息 + * @return apiVO.setObject1(User.class) 自己封装登录用户信息 + */ + ApiVO checkOpenId(ConsumerToken consumerToken); + + /** + * 没有账号时根据登录信息创建账号 + * + * @param consumerToken 登录信息 + * @return + */ + ApiVO addConsumer(ConsumerToken consumerToken); +} diff --git a/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/ApplicationRun.java b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/ApplicationRun.java index e46a16b..13c04ba 100644 --- a/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/ApplicationRun.java +++ b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/ApplicationRun.java @@ -2,8 +2,10 @@ package com.yexuejc.springboot.base; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration; -@SpringBootApplication +@SpringBootApplication(exclude = {RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class}) public class ApplicationRun { public static void main(String[] args) { SpringApplication.run(ApplicationRun.class, args); diff --git a/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/mapper/ConsumerMapper.java b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/mapper/ConsumerMapper.java new file mode 100644 index 0000000..2a0e76a --- /dev/null +++ b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/mapper/ConsumerMapper.java @@ -0,0 +1,39 @@ +package com.yexuejc.springboot.base.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.yexuejc.springboot.base.security.domain.Consumer; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + *

+ * Mapper 接口 + *

+ * + * @author yexuejc + * @since 2018-05-27 + */ +@Mapper +public interface ConsumerMapper extends BaseMapper { + + /** + *

+ * 插入一条记录 + *

+ * + * @param entity 实体对象 + * @return int + */ + @Override + int insert(Consumer entity); + + + /** + * 修改权限 + * + * @param entity + * @return + */ + Integer updateRoles(@Param(Constants.ENTITY) Consumer entity); +} diff --git a/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/mapper/handler/JsonTypeHandler.java b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/mapper/handler/JsonTypeHandler.java new file mode 100644 index 0000000..50a256a --- /dev/null +++ b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/mapper/handler/JsonTypeHandler.java @@ -0,0 +1,53 @@ +package com.yexuejc.springboot.base.mapper.handler; + +import com.yexuejc.base.util.JsonUtil; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedTypes; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * 数据库查询转换 -> JSON + */ +@MappedTypes({Object.class}) +public class JsonTypeHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) + throws SQLException { + if (parameter == null) { + ps.setString(i, null); + } else { + ps.setString(i, JsonUtil.obj2Json(parameter)); + } + } + + @Override + public Object getNullableResult(ResultSet rs, String columnName) throws SQLException { + if (rs.getString(columnName) == null) { + return null; + } + return JsonUtil.json2Obj(rs.getString(columnName), Object.class); + } + + @Override + public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + if (rs.getString(columnIndex) == null) { + return null; + } + return JsonUtil.json2Obj(rs.getString(columnIndex), Object.class); + } + + @Override + public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + if (cs.getString(columnIndex) == null) { + return null; + } + return JsonUtil.json2Obj(cs.getString(columnIndex), Object.class); + } + +} diff --git a/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/security/MySecurityConfig.java b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/security/MySecurityConfig.java new file mode 100644 index 0000000..59ac01d --- /dev/null +++ b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/security/MySecurityConfig.java @@ -0,0 +1,184 @@ +package com.yexuejc.springboot.base.security; + +import com.yexuejc.base.constant.RespsConsts; +import com.yexuejc.base.http.Resps; +import com.yexuejc.base.util.JsonUtil; +import com.yexuejc.base.util.JwtUtil; +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 com.yexuejc.springboot.base.exception.ThirdPartyAuthorizationException; +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.http.HttpStatus; +import org.springframework.security.authentication.*; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * @author maxf + * @version 1.0 + * @ClassName SecurityConfig + * @Description + * @date 2018/11/8 17:30 + */ +@EnableWebSecurity(debug = false) +public class MySecurityConfig extends SecurityConfig { + + @Autowired + @Qualifier("userserviceimpl") + UserService userService; + + @Override + protected UserService getUserService() { + return userService; + } + + @Autowired + @Qualifier(MutiRedisAutoConfiguration.BEAN_REDIS_TEMPLATE0) + private RedisTemplate redisTemplate0; + + /** + * 保存登录信息至redis + * + * @param redisTemplate0 redis 链接 + * @param user 登录用户 + * @param roles 角色信息 + * @param token 登录token + * @param isPast 是否设置过期 + */ + private void saveLoginUser(RedisTemplate redisTemplate0, ConsumerUser user, List roles, String token, + boolean isPast) { + Map m = new HashMap<>(4); + m.put("username", user.getUsername()); + m.put("token", token); + m.put("roles", roles); + m.put("id", user.getId()); + m.put("logType", user.getLogType()); + m.put("logTime", user.getLogTime()); + redisTemplate0.opsForHash().putAll(BizConsts.CONSUMER_LOGIN_REDIS + "." + user.getUsername(), m); + if (isPast) { + //对于没有绑定手机号的token,10分钟后过期 + redisTemplate0.expire(BizConsts.CONSUMER_LOGIN_REDIS + "." + user.getUsername(), 10, TimeUnit.MINUTES); + } + } + + /** + *
+     * 处理登录
+     * 成功: filter.setAuthenticationSuccessHandler()
+     * 失败: filter.setAuthenticationFailureHandler()
+     * 
+ * + * @param filter + */ + @Override + protected void loginHodler(ConsumerAuthenticationProcessingFilter filter) { + filter.setAuthenticationSuccessHandler((request, response, authentication) -> { + String token = JwtUtil.instace().compact(new LoginToken(authentication.getName())); + ConsumerUser user = (ConsumerUser) authentication.getPrincipal(); + Collection authorities = authentication.getAuthorities(); + List roles = new ArrayList<>(); + if (authorities != null && authorities.size() > 0) { + for (GrantedAuthority g : authorities) { + roles.add(g.getAuthority()); + } + } + Map map = new HashMap<>(2); + map.put("token", token); + map.put("bindMobile", false); + if (StrUtil.isEmpty(user.getUsername()) || !RegexUtil.regex(user.getUsername(), RegexUtil.REGEX_MOBILE)) { + map.put("bindMobile", true); + } + saveLoginUser(redisTemplate0, user, roles, token, (Boolean) map.get("bindMobile")); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(JsonUtil.obj2Json(Resps.success().setSucc(map))); + response.getWriter().close(); + }); + 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 { + resps.setErr(RespsConsts.CODE_FAIL, new String[]{BizConsts.BASE_SYS_ERR_MSG}); + } + response.getWriter().write(JsonUtil.obj2Json(resps)); + response.getWriter().close(); + }); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + super.configure(http); + http.headers().frameOptions().disable(); + /** + * 权限控制 + * ->无权限 + */ + http.authorizeRequests().antMatchers( + "/", "/index" + ).permitAll(); + + /** + * 权限控制 + * ->登录可访问 + */ + http.authorizeRequests().antMatchers( + "/consumer/**", "/sms/bind/**").authenticated(); + + // 登出处理。 + http.logout().logoutSuccessHandler((request, response, authentication) -> { + redisTemplate0.delete(BizConsts.CONSUMER_LOGIN_REDIS + "." + authentication.getName()); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(JsonUtil.obj2Json(Resps.success())); + response.getWriter().close(); + }); + + // 未登录,却访问需要登录的接口时的处理 + http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(401); + response.getWriter().write( + JsonUtil.obj2Json( + Resps.error(RespsConsts.CODE_FAIL, new String[]{BizConsts.BASE_NOT_LOGIN_MSG}) + ) + ); + response.getWriter().close(); + }); + // 已登录,但当前用户没有访问的某个接口的权限时的处理 + http.exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(401); + response.getWriter().write( + JsonUtil.obj2Json( + Resps.error(RespsConsts.CODE_FAIL, new String[]{BizConsts.BASE_NOT_ROLE_MSG}) + ) + ); + response.getWriter().close(); + }); + } +} 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 new file mode 100644 index 0000000..06f0c0c --- /dev/null +++ b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/security/UserServiceImpl.java @@ -0,0 +1,337 @@ +package com.yexuejc.springboot.base.security; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.yexuejc.base.constant.RespsConsts; +import com.yexuejc.base.pojo.ApiVO; +import com.yexuejc.base.util.StrUtil; +import com.yexuejc.springboot.base.autoconfigure.MutiRedisAutoConfiguration; +import com.yexuejc.springboot.base.constant.BizConsts; +import com.yexuejc.springboot.base.constant.DictRegTypeConsts; +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.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * 数据库操作DB + * + * @author maxf + * @version 1.0 + * @ClassName UserServiceImpl + * @Description + * @date 2018/11/8 17:31 + */ +@Service("userserviceimpl") +public class UserServiceImpl implements UserService { + @Autowired + ConsumerMapper consumerMapper; + + @Autowired + @Qualifier(MutiRedisAutoConfiguration.BEAN_REDIS_TEMPLATE1) + private RedisTemplate redisTemplate; + + /** + * 根据用户名到数据库查询用户 + * + * @param username 登录账号 + * @return + */ + @Override + public User getConsumerByUserName(String username) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq("mobile", username); + Consumer consumer = consumerMapper.selectOne(queryWrapper); + return consumer; + } + + /** + * 到自己的reids里面校验短信验证码 + * + * @param smsType {@linkplain BizConsts.CONSUMER_LOGIN_SMS} 业务id reids使用 + * @param mobile 登录账号 + * @param code 短信验证码 + * @return + */ + @Override + public ApiVO checkSmsCode2Redis(String smsType, String mobile, String code) { + redisTemplate.afterPropertiesSet(); + String rCode = (String) redisTemplate.opsForHash().get(smsType + "." + mobile, "code"); + Integer validatedNums = (Integer) redisTemplate.opsForHash().get(smsType + "." + mobile, "validatedNums"); + if (validatedNums == null) { + return new ApiVO(ApiVO.STATUS.F, RespsConsts.CODE_FAIL, "验证码过期,请重新获取"); + } else if (validatedNums > 5) { + redisTemplate.delete(smsType + "." + mobile); + return new ApiVO(ApiVO.STATUS.F, RespsConsts.CODE_FAIL, "验证码过期,请重新获取"); + } + if (code.equals(rCode)) { + redisTemplate.delete(smsType + "." + mobile); + return new ApiVO(ApiVO.STATUS.S); + } else { + validatedNums++; + redisTemplate.opsForHash().put(smsType + "." + mobile, "validatedNums", validatedNums); + return new ApiVO(ApiVO.STATUS.F, RespsConsts.CODE_FAIL, "验证码不正确"); + } + } + + /** + * 校验openid 根据自己业务做判断 + *
+ * 返回:封装登录用户信息到 apiVO.setObject1(User.class) 自己封装登录用户信息 + * + * @param consumerToken 登录信息 + * @return + */ + @Override + public ApiVO checkOpenId(ConsumerToken consumerToken) { + ApiVO apiVO = new ApiVO(ApiVO.STATUS.F, "没有找到用户信息"); + switch (consumerToken.getLogtype()) { + case LogTypeConsts.QQ: + apiVO = checkOpenid4QQ(consumerToken, true); + break; + case LogTypeConsts.WECHAT: + apiVO = checkOpenid4Wechat(consumerToken, true); + break; + case LogTypeConsts.WEIBO: + apiVO = checkOpenid4Weibo(consumerToken, true); + break; + default: + break; + } + return apiVO; + } + + /** + * 第三方登录 QQ登录 + * + * @param consumerToken 登录信息 + * @param b + * @return + */ + public ApiVO checkOpenid4QQ(ConsumerToken consumerToken, boolean b) { + Consumer consumer = getConsumerByQQOpenid(consumerToken.getOpenid()); + if (consumer == null) { + return new ApiVO(ApiVO.STATUS.F, "获取用户信息失败"); + } + if (b && DictRegTypeConsts.DICT_QQ.equals(consumer.getRegType())) { + //如果是qq注册的,登录的同时更新用户信息 + updateConsumer(consumer, consumerToken); + } + return new ApiVO(ApiVO.STATUS.S).setObject1(consumer); + } + + /** + * 第三方登录 微信登录 + * + * @param consumerToken 登录信息 + * @param b + * @return + */ + public ApiVO checkOpenid4Wechat(ConsumerToken consumerToken, boolean b) { + Consumer consumer = getConsumerByWechatOpenid(consumerToken.getOpenid()); + if (consumer == null) { + return new ApiVO(ApiVO.STATUS.F, "获取用户信息失败"); + } + if (b && DictRegTypeConsts.DICT_WECHAT.equals(consumer.getRegType())) { + //如果是微信注册的,登录的同时更新用户信息 + updateConsumer(consumer, consumerToken); + } + return new ApiVO(ApiVO.STATUS.S).setObject1(consumer); + } + + + /** + * 第三方登录 微博登录 + * + * @param consumerToken 登录信息 + * @param b + * @return + */ + public ApiVO checkOpenid4Weibo(ConsumerToken consumerToken, boolean b) { + Consumer consumer = getConsumerByWeiboOpenid(consumerToken.getOpenid()); + if (consumer == null) { + return new ApiVO(ApiVO.STATUS.F, "获取用户信息失败"); + } + if (b && DictRegTypeConsts.DICT_WEIBO.equals(consumer.getRegType())) { + //如果是微博注册的,登录的同时更新用户信息 + updateConsumer(consumer, consumerToken); + } + return new ApiVO(ApiVO.STATUS.S).setObject1(consumer); + } + + + /** + * 没有账号时处理自己的业务,此次必须返回 构造出的登录用户,否则会抛出{@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); + Consumer consumer = consumerMapper.selectOne(queryWrapper); + return consumer; + } + + public Consumer getConsumerByWechatOpenid(String openid) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("wechat_id", openid); + Consumer consumer = consumerMapper.selectOne(queryWrapper); + return consumer; + } + + public Consumer getConsumerByWeiboOpenid(String openid) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("weibo_id", openid); + Consumer consumer = consumerMapper.selectOne(queryWrapper); + return consumer; + } + + + /** + * 更新基本信息 + * + * @param consumer + * @param consumerToken + */ + private void updateConsumer(Consumer consumer, ConsumerToken consumerToken) { + boolean b1, b2, b3; + b1 = b2 = b3 = true; + if (StrUtil.isNotEmpty(consumerToken.getNickname())) { + if (consumerToken.getNickname().equals(consumer.getNickname())) { + b1 = false; + } + consumer.setNickname(consumerToken.getNickname()); + } + b2 = setHeader(consumerToken, consumer, true); + b3 = setSex(consumerToken, consumer); + if (!b1 && !b2 && !b3) { + return; + } + LambdaUpdateWrapper queryWrapper = new UpdateWrapper<>(new Consumer()).lambda(); + try { + queryWrapper.set(Consumer::getNickname, consumer.getNickname()) + .set(Consumer::getHead, consumer.getHead()) + .set(Consumer::getSex, consumer.getSex()) + .eq(Consumer::getConsumerId, consumer.getConsumerId()); + consumerMapper.update(new Consumer(), queryWrapper); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 设置头像->上传网络图片到OSS + * + * @param consumerToken + * @param consumer + * @param isUpdate + */ + private boolean setHeader(ConsumerToken consumerToken, Consumer consumer, boolean isUpdate) { + if (StrUtil.isNotEmpty(consumerToken.getHead())) { + if (isUpdate) { + if (consumerToken.getHead().equals(consumer.getSourceHead())) { + //未改变头像 + return false; + } + } else { + consumer.setSourceHead(consumerToken.getHead()); + } + //应该上传至OSS后返回OSS地址 + consumer.setHead(consumerToken.getHead()); +// try { +// consumer.setHead(putOss4Head(null, consumerToken.getHead())); +// } catch (ImageException e) { +// return false; +// } + } + return true; + } + + /** + * 设置性别 + * + * @param consumerToken + * @param consumer + */ + private boolean setSex(ConsumerToken consumerToken, Consumer consumer) { + if (StrUtil.isNotEmpty(consumerToken.getSex())) { + if ("1".equals(consumerToken.getSex()) && !"男".equals(consumer.getSex())) { + consumer.setSex("男"); + return true; + } else if ("2".equals(consumerToken.getSex()) && !"女".equals(consumer.getSex())) { + consumer.setSex("女"); + return true; + } + } + return false; + } + +} diff --git a/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/security/domain/Consumer.java b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/security/domain/Consumer.java new file mode 100644 index 0000000..5ab3da0 --- /dev/null +++ b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/security/domain/Consumer.java @@ -0,0 +1,281 @@ +package com.yexuejc.springboot.base.security.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.activerecord.Model; +import com.yexuejc.base.util.JsonUtil; +import com.yexuejc.springboot.base.security.inte.User; + +import java.io.Serializable; +import java.util.List; + +/** + *

+ *

+ *

+ * + * @author yexuejc + * @since 2018-05-27 + */ +@TableName(resultMap = "BaseResultMap") +public class Consumer extends Model implements User { + + private static final long serialVersionUID = 1L; + + /** + * 用户id + */ + @TableId(value = "consumer_id", type = IdType.UUID) + private String consumerId; + /** + * 手机号 + */ + @TableField("mobile") + private String mobile; + /** + * 密码:md5 + */ + @TableField("pwd") + private String pwd; + /** + * 账号是否启用 + */ + @TableField("is_enable") + private boolean enable; + /** + * 账号是否没有过期 + */ + @TableField("is_non_expire") + private boolean nonExpire; + /** + * 账号是否没有被锁定 + */ + @TableField("is_non_lock") + private boolean nonLock; + /** + * 微信id + */ + @TableField("wechat_id") + private String wechatId; + /** + * qq id + */ + @TableField("qq_id") + private String qqId; + /** + * 微博id + */ + @TableField("weibo_id") + private String weiboId; + /** + * 昵称 + */ + @TableField("nickname") + private String nickname; + /** + * 用户头像 + */ + @TableField("head") + private String head; + /** + * 用户邮箱 + */ + @TableField("email") + private String email; + /** + * 用户姓别 '男',‘女’ + */ + @TableField("sex") + private String sex; + /** + * 角色、权限 + */ + @TableField(value = "roles", el = "roles,typeHandler=com.yexuejc.guansc.core.mybatis.handler.JsonTypeHandler") + private List roles; + /** + * 支付密码 + */ + @TableField("pay_pwd") + private String payPwd; + /** + * 注册方式 + */ + @TableField("reg_type") + private String regType; + /** + * 第三方源头像路径 + */ + @TableField("source_head") + private String sourceHead; + + @Override + public String toString() { + return JsonUtil.obj2Json(this); + } + + public String getConsumerId() { + return consumerId; + } + + public Consumer setConsumerId(String consumerId) { + this.consumerId = consumerId; + return this; + } + + public String getMobile() { + return mobile; + } + + public Consumer setMobile(String mobile) { + this.mobile = mobile; + return this; + } + + public String getPwd() { + return pwd; + } + + @Override + public boolean getEnable() { + return this.enable; + } + + @Override + public boolean getNonExpire() { + return this.nonExpire; + } + + @Override + public boolean getNonLock() { + return this.nonLock; + } + + public Consumer setPwd(String pwd) { + this.pwd = pwd; + return this; + } + + + public Consumer setEnable(boolean enable) { + this.enable = enable; + return this; + } + + + public Consumer setNonExpire(boolean nonExpire) { + this.nonExpire = nonExpire; + return this; + } + + + public Consumer setNonLock(boolean nonLock) { + this.nonLock = nonLock; + return this; + } + + public String getWechatId() { + return wechatId; + } + + public Consumer setWechatId(String wechatId) { + this.wechatId = wechatId; + return this; + } + + public String getQqId() { + return qqId; + } + + public Consumer setQqId(String qqId) { + this.qqId = qqId; + return this; + } + + public String getWeiboId() { + return weiboId; + } + + public Consumer setWeiboId(String weiboId) { + this.weiboId = weiboId; + return this; + } + + public String getNickname() { + return nickname; + } + + public Consumer setNickname(String nickname) { + this.nickname = nickname; + return this; + } + + public String getHead() { + return head; + } + + public Consumer setHead(String head) { + this.head = head; + return this; + } + + public String getEmail() { + return email; + } + + public Consumer setEmail(String email) { + this.email = email; + return this; + } + + public String getSex() { + return sex; + } + + public Consumer setSex(String sex) { + this.sex = sex; + return this; + } + + public List getRoles() { + return roles; + } + + public Consumer setRoles(List roles) { + this.roles = roles; + return this; + } + + public String getPayPwd() { + return payPwd; + } + + public Consumer setPayPwd(String payPwd) { + this.payPwd = payPwd; + return this; + } + + public String getRegType() { + return regType; + } + + public Consumer setRegType(String regType) { + this.regType = regType; + return this; + } + + public String getSourceHead() { + return sourceHead; + } + + public Consumer setSourceHead(String sourceHead) { + this.sourceHead = sourceHead; + return this; + } + + @Override + protected Serializable pkVal() { + return this.consumerId; + } +} diff --git a/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/web/IndexCtrl.java b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/web/IndexCtrl.java index 451bd18..943b957 100644 --- a/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/web/IndexCtrl.java +++ b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/web/IndexCtrl.java @@ -94,4 +94,5 @@ public class IndexCtrl { return Resps.success().setSucc(map); } + } 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 new file mode 100644 index 0000000..19a486c --- /dev/null +++ b/yexuejc-springboot-base/src/test/java/com/yexuejc/springboot/base/web/SecurityCtrl.java @@ -0,0 +1,22 @@ +package com.yexuejc.springboot.base.web; + +import org.springframework.web.bind.annotation.RestController; + +/** + *
+ * Security 登录注册相关controller
+ * 主要实现
+ * 1.短信登录发送验证码
+ * 2.第三方登录绑定手机号(以及绑定手机号发送验证码)
+ * 
+ * + * @author maxf + * @version 1.0 + * @ClassName SecurityCtrl + * @Description + * @date 2018/11/9 10:52 + */ +@RestController +public class SecurityCtrl { + +} diff --git a/yexuejc-springboot-base/src/test/resources/application.properties b/yexuejc-springboot-base/src/test/resources/application.properties index 74813f1..b7da733 100644 --- a/yexuejc-springboot-base/src/test/resources/application.properties +++ b/yexuejc-springboot-base/src/test/resources/application.properties @@ -1,38 +1,36 @@ server.port=8888 - spring.application.name=@pom.artifactId@ +#log logging.level.root=info logging.path=/logs/yexuejc-springboot-parent -yexuejc.http.filter.type=0 +yexuejc.http.filter.type=1 yexuejc.http.encrypt.encrypt=true yexuejc.http.encrypt.decrypt=true -#配置密钥方式 +#Կʽ #yexuejc.http.encrypt.private-key=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAiSo5blJ9-QJ0_QElcy5AaRTq-3oO4lJ8PvIOIt-Xr5SUFODVj3DUbiy6_0bxQYO3NiYHlXPb37UVV3jjlXJsXwIDAQABAkBE0WOJH2hGs93gRl_0vwLf9ffDfkTTdlER_73p70aad3QZRslEkinQH7G5aE_DgBm5m72TCeH-PD2FZ2lwtavBAiEAvnRown5Lpqbl0tN_OUxr_e1u9d_-8dNL_JEETO7BZCECIQC4XtY-18j0bVVLxaXPjKQ00D59yntwObihDNyRK0nAfwIgHPHEGgrnpGQo-Wl7JFIg925mNqfcLxRVsAS6CpcefQECIQCUsLdsmy6QIhTmNRJSXoSXq1KatE_05DhIekzwLs8eFQIgfMawMiu52ZxBI5_pZ7ancQZ6Dsxl45utFqJShzV1pio #yexuejc.http.encrypt.public-key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAIkqOW5SffkCdP0BJXMuQGkU6vt6DuJSfD7yDiLfl6-UlBTg1Y9w1G4suv9G8UGDtzYmB5Vz29-1FVd445VybF8CAwEAAQ -#配置证书方式 +#֤鷽ʽ yexuejc.http.encrypt.private-key-path=/lgfishing.keystore yexuejc.http.encrypt.private-alias=lgfishing yexuejc.http.encrypt.private-pwd=lgfishing2018 -#编码 +# spring.http.encoding.force=true spring.http.encoding.charset=UTF-8 spring.http.encoding.enabled=true server.tomcat.uri-encoding=UTF-8 -#是否开启HTTPS(SSL)请求证书验证忽略:默认false +#ǷHTTPSSSL֤֤ԣĬfalse yexuejc.enable.ssl-ignore=true -#reids -spring.redis.host=121.42.165.89 -spring.redis.password= -spring.redis.port=16379 + + #mns @@ -46,3 +44,50 @@ yexuejc.alibaba.oss.endpoint=oss-cn-beijing.aliyuncs.com yexuejc.alibaba.oss.access-key-secret= yexuejc.alibaba.oss.access-key-id= yexuejc.alibaba.oss.bucket=guansichou + + + +#======================================================================================================================== +# security +#reids +#ָredisdb0ĬϿ +yexuejc.redis.db1=true +spring.redis.jedis.pool.max-active=100 +spring.redis.jedis.pool.max-idle=10 +spring.redis.jedis.pool.min-idle=3 +spring.redis.host=121.42.165.89 +spring.redis.password= +spring.redis.port=16379 + + +#db + +spring.h2.console.path=/h2-console +spring.h2.console.enabled=true +spring.h2.console.settings.web-allow-others=true +spring.datasource.username=sa +spring.datasource.password=123456 +spring.datasource.url=jdbc:h2:mem:test;MODE=PostgreSQL +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.schema=classpath:db/schema.sql +spring.datasource.data=classpath:db/data.sql + +#======================================================================================================================== +#mybatis-plus +mybatis-plus.mapper-locations=classpath*:mapper/*.xml +#ʵɨ裬packageöŻ߷ֺŷָ +mybatis-plus.type-aliases-package=com.yexuejc.springboot.base.security.domain +#0:"ݿID", 1:"ûID",2:"Ϊδ", 3:"ȫΨһID UUID",4:ȫΨһID (UUID),5:ַȫΨһID (idWorker ַʾ); +mybatis-plus.global-config.db-config.id-type=uuid +mybatis-plus.global-config.db-config.db-type=POSTGRE_SQL +#ֶβ 0:"ж",1:" NULL ж"),2:"ǿж" +mybatis-plus.global-config.db-config.field-strategy=not_empty +#շ»ת +mybatis-plus.global-config.db-config.column-underline=true +#߼ɾã3ã +mybatis-plus.global-config.db-config.logic-delete-value=true +mybatis-plus.global-config.db-config.logic-not-delete-value=false +#÷ݿ(column»&&javaʵշ)ԶƥasûSQLҪдas select user_id as userId +mybatis-plus.configuration.map-underscore-to-camel-case=true +mybatis-plus.configuration.cache-enabled=false +#======================================================================================================================== diff --git a/yexuejc-springboot-base/src/test/resources/db/data.sql b/yexuejc-springboot-base/src/test/resources/db/data.sql new file mode 100644 index 0000000..bae2350 --- /dev/null +++ b/yexuejc-springboot-base/src/test/resources/db/data.sql @@ -0,0 +1,34 @@ +INSERT INTO consumer (consumer_id, + mobile, + pwd, + is_enable, + is_non_expire, + is_non_lock, + wechat_id, + qq_id, + weibo_id, + nickname, + head, + email, + sex, + roles, + pay_pwd, + reg_type, + source_head) +VALUES ('119d8c62b8b04154a073b3f6c3d9e14f', + '18202837563', + 'e10adc3949ba59abbe56e057f20f883e', + 't', + 't', + 't', + 'ogIXq0HrDq3kS6MAHqMI1RUqBrGw', + NULL, + NULL, + '18202837563', + 'head/6dc93e2e0809426ca8a9e6ede30b0b50.JPEG', + NULL, + '男', + '[ROLE_CONSUMER, ROLE_LAYER]', + NULL, + 'S', + 'https://wx.qlogo.cn/mmopen/vi_32/MftzC3yHluDbjZ9c3sYEibUuXNkC3pha8E6pibZO3Wh0Zop0bqHLcltjmrENc4R8Xm6oJECQGibAxZot1v9PR1hsw/132'); diff --git a/yexuejc-springboot-base/src/test/resources/db/schema.sql b/yexuejc-springboot-base/src/test/resources/db/schema.sql new file mode 100644 index 0000000..dc9f36d --- /dev/null +++ b/yexuejc-springboot-base/src/test/resources/db/schema.sql @@ -0,0 +1,42 @@ +CREATE TABLE consumer ( + consumer_id varchar(32) NOT NULL DEFAULT NULL::character varying, + mobile varchar(50) NOT NULL DEFAULT NULL::character varying, + pwd varchar(32) DEFAULT NULL::character varying, + is_enable bool DEFAULT true, + is_non_expire bool DEFAULT true, + is_non_lock bool DEFAULT true, + wechat_id varchar(50) DEFAULT NULL::character varying, + qq_id varchar(50) DEFAULT NULL::character varying, + weibo_id varchar(50) DEFAULT NULL::character varying, + nickname varchar(50) DEFAULT NULL::character varying, + head varchar(255) DEFAULT NULL::character varying, + email varchar(32) DEFAULT NULL::character varying, + sex varchar(1) DEFAULT NULL::character varying, + roles varchar(255), + pay_pwd varchar(32) DEFAULT NULL::character varying, + reg_type varchar(10) , + source_head varchar(255) +) +; +COMMENT ON COLUMN consumer.consumer_id IS '用户id'; +COMMENT ON COLUMN consumer.mobile IS '手机号'; +COMMENT ON COLUMN consumer.pwd IS '密码:md5'; +COMMENT ON COLUMN consumer.is_enable IS '账号是否启用'; +COMMENT ON COLUMN consumer.is_non_expire IS '账号是否没有过期'; +COMMENT ON COLUMN consumer.is_non_lock IS '账号是否没有被锁定'; +COMMENT ON COLUMN consumer.wechat_id IS '微信id'; +COMMENT ON COLUMN consumer.qq_id IS 'qq id'; +COMMENT ON COLUMN consumer.weibo_id IS '微博id'; +COMMENT ON COLUMN consumer.nickname IS '昵称'; +COMMENT ON COLUMN consumer.head IS '用户头像'; +COMMENT ON COLUMN consumer.email IS '用户邮箱'; +COMMENT ON COLUMN consumer.sex IS '用户姓别 ''男'',‘女’'; +COMMENT ON COLUMN consumer.roles IS '角色、权限'; +COMMENT ON COLUMN consumer.pay_pwd IS '支付密码'; +COMMENT ON COLUMN consumer.reg_type IS '注册方式'; +COMMENT ON COLUMN consumer.source_head IS '第三方源头像路径'; + +-- ---------------------------- +-- Primary Key structure for table consumer +-- ---------------------------- +ALTER TABLE consumer ADD CONSTRAINT consumer_pkey PRIMARY KEY (consumer_id); diff --git a/yexuejc-springboot-base/src/test/resources/mapper/ConsumerMapper.xml b/yexuejc-springboot-base/src/test/resources/mapper/ConsumerMapper.xml new file mode 100644 index 0000000..a4c4eab --- /dev/null +++ b/yexuejc-springboot-base/src/test/resources/mapper/ConsumerMapper.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO consumer (consumer_id, + mobile, + pwd, + is_enable, + is_non_expire, + is_non_lock, + wechat_id, + qq_id, + weibo_id, + nickname, + head, + email, + sex, + roles, + pay_pwd, + reg_type, + source_head) + VALUES (#{consumerId}, + #{mobile}, + #{pwd}, + #{enable}, + #{nonExpire}, + #{nonLock}, + #{wechatId}, + #{qqId}, + #{weiboId}, + #{nickname}, + #{head}, + #{email}, + #{sex}, + #{roles,typeHandler=com.yexuejc.springboot.base.mapper.handler.JsonTypeHandler}, + #{payPwd}, + #{regType}, + #{sourceHead}); + + + + + + update consumer + + + roles = #{et.roles,typeHandler=com.yexuejc.springboot.base.mapper.handler.JsonTypeHandler}, + + mdfy_time=now(), + mdfy_by=#{et.mdfyBy} + + where + consumer_id=#{et.consumerId} + + +