├── .gitattributes ├── src └── main │ └── java │ ├── org │ └── yangyuan │ │ └── security │ │ ├── core │ │ ├── common │ │ │ ├── SecuritySerializable.java │ │ │ ├── CacheManager.java │ │ │ ├── Cookie.java │ │ │ ├── ResourceFactory.java │ │ │ ├── ConcurrentSubjectControl.java │ │ │ ├── SecurityToken.java │ │ │ ├── Subject.java │ │ │ ├── PrincipalFactory.java │ │ │ ├── AbstractSecurityToken.java │ │ │ ├── Session.java │ │ │ ├── SecurityAuthHandler.java │ │ │ ├── SecurityManager.java │ │ │ ├── AbstractCookie.java │ │ │ └── PasswordManager.java │ │ ├── MultiportConcurrentSubjectControl.java │ │ ├── SingleConcurrentSubjectControl.java │ │ ├── SessionManager.java │ │ ├── DefaultPrincipalFactory.java │ │ ├── RefuseConcurrentSubjectControl.java │ │ ├── PrincipalInvalidCookie.java │ │ ├── QqRemoteToken.java │ │ ├── PrincipalPersistentCookie.java │ │ ├── WbRemoteToken.java │ │ ├── PrincipalSessionCookie.java │ │ ├── JdbcToken.java │ │ ├── annotation │ │ │ ├── SecurityFilterComponent.java │ │ │ └── Security.java │ │ ├── CaptchaToken.java │ │ ├── UsernamePasswordToken.java │ │ ├── RemoteToken.java │ │ ├── WxRemoteToken.java │ │ ├── DefaultSession.java │ │ ├── SecurityFilterManager.java │ │ ├── DefaultCacheManager.java │ │ ├── DefaultSecurityManager.java │ │ └── DefaultSubject.java │ │ ├── bean │ │ ├── common │ │ │ └── RoleAdaptor.java │ │ ├── BasicAuth.java │ │ ├── User.java │ │ └── Permission.java │ │ ├── dao │ │ ├── common │ │ │ ├── RedisResourceFactory.java │ │ │ ├── StatisticalSessionDao.java │ │ │ ├── AuthSessionDao.java │ │ │ └── CacheSessionDao.java │ │ ├── RemoteSessionDao.java │ │ ├── JdbcSessionDao.java │ │ └── EhcacheSessionDao.java │ │ ├── realm │ │ ├── common │ │ │ ├── Realm.java │ │ │ ├── JdbcRealmAdaptor.java │ │ │ ├── CaptchaRealmAdaptor.java │ │ │ ├── RemoteRealmAdaptor.java │ │ │ └── AbstractRealm.java │ │ ├── bean │ │ │ ├── RemoteUserAdaptor.java │ │ │ ├── CaptchaUserAdaptor.java │ │ │ ├── JdbcUser.java │ │ │ ├── JdbcUserAdaptor.java │ │ │ ├── CaptchaUser.java │ │ │ ├── UserAdaptor.java │ │ │ └── RemoteUser.java │ │ ├── captcha │ │ │ └── CaptchaRealm.java │ │ ├── jdbc │ │ │ └── JdbcRealm.java │ │ └── remote │ │ │ └── RemoteRealm.java │ │ ├── captcha │ │ └── common │ │ │ ├── SecurityImageCaptcha.java │ │ │ ├── SecurityCaptcha.java │ │ │ ├── AbstractPhoneEmailSecurityCaptcha.java │ │ │ ├── AbstractSecurityCaptcha.java │ │ │ └── AbstractSecurityImageCaptcha.java │ │ ├── exception │ │ ├── common │ │ │ ├── FilterException.java │ │ │ ├── CaptchaException.java │ │ │ ├── AuthenticationException.java │ │ │ ├── SecurityException.java │ │ │ └── AuthenticationBusinessException.java │ │ ├── AuthRemoteFailException.java │ │ ├── SecurityFilterAuthException.java │ │ ├── CaptchaSendTooFastException.java │ │ ├── AuthPasswordWrongException.java │ │ ├── SecurityFilterErrorException.java │ │ ├── AuthUserDisabledException.java │ │ ├── AuthUsernameNotFoundException.java │ │ ├── SecurityFilterForbiddenException.java │ │ ├── SecurityFilterBasicAuthException.java │ │ ├── ConcurrentSubjectControlException.java │ │ └── CaptchaUnknownAccountTypeException.java │ │ ├── config │ │ ├── proxy │ │ │ ├── RedisResourceFactoryProxy.java │ │ │ ├── JdbcRealmAdaptorProxy.java │ │ │ ├── RemoteRealmAdaptorProxy.java │ │ │ ├── CaptchaRealmAdaptorProxy.java │ │ │ └── SecurityAuthHandlerProxy.java │ │ ├── CommonResource.java │ │ ├── SessionResource.java │ │ ├── CaptchaResource.java │ │ ├── CookieResource.java │ │ ├── CacheResource.java │ │ └── CoreResource.java │ │ ├── filter │ │ ├── AnonSecurityFilter.java │ │ ├── common │ │ │ ├── SecurityFilter.java │ │ │ └── AbstractSecurityCacheFilter.java │ │ ├── AuthcSecurityFilter.java │ │ ├── RoleSecurityFilter.java │ │ └── BasicHttpAuthenticationSecurityFilter.java │ │ ├── util │ │ ├── SecurityHexUtil.java │ │ ├── SecurityDate.java │ │ ├── SecuritySort.java │ │ ├── SecurityMD5.java │ │ └── SecurityConfigUtils.java │ │ ├── http │ │ ├── client │ │ │ ├── common │ │ │ │ ├── HttpOptions.java │ │ │ │ ├── AbstractSSLHttpClient.java │ │ │ │ └── AbstractHttpClient.java │ │ │ └── HttpClient.java │ │ └── response │ │ │ └── SimpleResponse.java │ │ ├── spring │ │ └── SecuritySpringHook.java │ │ └── servlet │ │ └── SecurityInterceptor.java │ └── security.properties ├── .gitignore ├── README.md ├── .project ├── .classpath └── pom.xml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/common/SecuritySerializable.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core.common; 2 | 3 | /** 4 | * 序列化定义 5 | * @author yangyuan 6 | * @date 2018年4月10日 7 | */ 8 | public interface SecuritySerializable { 9 | /** 10 | * 序列化 11 | * @return 字节数组 12 | */ 13 | byte[] getBytes(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/bean/common/RoleAdaptor.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.bean.common; 2 | 3 | /** 4 | * 用户角色模型适配器 5 | * @author yangyuan 6 | * @date 2018年6月14日 7 | */ 8 | public interface RoleAdaptor { 9 | 10 | /** 11 | * 获取角色名称 12 | * @return 角色名称 13 | */ 14 | String getRole(); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/common/CacheManager.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core.common; 2 | 3 | /** 4 | * 缓存管理器定义 5 | * @author yangyuan 6 | * @date 2018年3月30日 7 | */ 8 | public interface CacheManager { 9 | /** 10 | * 强制主题缓存失效 11 | * @param subject 主题 12 | */ 13 | void invalid(Subject subject); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/common/Cookie.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core.common; 2 | 3 | /** 4 | * security cookie定义 5 | * @author yangyuan 6 | * @date 2018年3月5日 7 | */ 8 | public interface Cookie { 9 | /** 10 | * 转换为Http协议标准Cookie 11 | * @return Http协议标准Cookie 12 | */ 13 | javax.servlet.http.Cookie toHttpCookie(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/common/ResourceFactory.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core.common; 2 | 3 | /** 4 | * 资源工厂定义 5 | * @author yangyuan 6 | * @param 资源类型 7 | * @date 2018年3月31日 8 | */ 9 | public interface ResourceFactory { 10 | 11 | /** 12 | * 获取资源 13 | * @return 资源实例 14 | */ 15 | T getResource(); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/dao/common/RedisResourceFactory.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.dao.common; 2 | 3 | import org.yangyuan.security.core.common.ResourceFactory; 4 | 5 | import redis.clients.jedis.Jedis; 6 | 7 | /** 8 | * redis连接工厂定义 9 | * @author yangyuan 10 | * @date 2018年3月31日 11 | */ 12 | public interface RedisResourceFactory extends ResourceFactory{ 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/common/ConcurrentSubjectControl.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core.common; 2 | 3 | import org.yangyuan.security.bean.User; 4 | 5 | /** 6 | * 并发安全认证主题控制定义 7 | * @author yangyuan 8 | * @date 2018年4月25日 9 | */ 10 | public interface ConcurrentSubjectControl { 11 | /** 12 | * 控制 13 | * @param user 用户模型 14 | */ 15 | void control(User user); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/common/SecurityToken.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core.common; 2 | 3 | /** 4 | * 认证令牌定义 5 | * @author yangyuan 6 | * @date 2017年4月26日 7 | */ 8 | public interface SecurityToken { 9 | /** 10 | * 记住登陆状态标记 11 | * @return 12 | *

true 用户勾选remember me,记住登陆状态

13 | *

false 其他情况视为临时登陆,关闭浏览器窗口后自动退出登陆

14 | */ 15 | boolean isRemember(); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/dao/common/StatisticalSessionDao.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.dao.common; 2 | 3 | /** 4 | * 统计数据访问层定义 5 | * @author yangyuan 6 | * @date 2018年4月8日 7 | */ 8 | public interface StatisticalSessionDao { 9 | 10 | /** 11 | * 获取在线人数 12 | * @return 在线人数,不要求精确值 13 | */ 14 | long numberOfOnline(); 15 | 16 | /** 17 | * 获取活跃人数 18 | * @return 活跃人数,不要求精确值 19 | */ 20 | long numberOfActivity(); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/realm/common/Realm.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.realm.common; 2 | 3 | import org.yangyuan.security.bean.User; 4 | import org.yangyuan.security.core.common.SecurityToken; 5 | 6 | /** 7 | * 数据源定义 8 | * @author yangyuan 9 | * @date 2018年3月15日 10 | */ 11 | public interface Realm { 12 | 13 | /** 14 | * 获取认证系统用户 15 | * @param token 令牌 16 | * @return 认证系统用户,实例中必须包含[用户全局唯一id(unionid)]和[用户角色列表(roles)] 17 | */ 18 | User getUser(SecurityToken token); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/MultiportConcurrentSubjectControl.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | import org.yangyuan.security.bean.User; 4 | import org.yangyuan.security.core.common.ConcurrentSubjectControl; 5 | 6 | /** 7 | * 多端登陆并发安全认证主题控制实现 8 | *

允许同一个账号同时在不同客户端登陆

9 | * @author yangyuan 10 | * @date 2018年4月25日 11 | */ 12 | public class MultiportConcurrentSubjectControl implements ConcurrentSubjectControl{ 13 | 14 | @Override 15 | public void control(User user) { 16 | return; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/dao/common/AuthSessionDao.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.dao.common; 2 | 3 | import org.yangyuan.security.bean.User; 4 | import org.yangyuan.security.core.common.SecurityToken; 5 | 6 | /** 7 | * 认证数据访问层定义 8 | * @author yangyuan 9 | * @date 2017年4月26日 10 | */ 11 | public interface AuthSessionDao { 12 | 13 | /** 14 | * 认证 15 | * @param token 令牌 16 | * @return 正常执行,认证成功。
认证失败会抛出相应的运行时异常。
实例中必须包含[用户全局唯一id(unionid)]和[用户角色列表(roles)]。 17 | */ 18 | User auth(SecurityToken token); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/realm/bean/RemoteUserAdaptor.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.realm.bean; 2 | 3 | /** 4 | * 第三方授权用户数据适配器 5 | * @author yangyuan 6 | * @date 2018年5月31日 7 | */ 8 | public class RemoteUserAdaptor extends UserAdaptor{ 9 | /** 10 | * 构造方法 11 | * @param unionid 用户全局唯一id 12 | * @param roles 用户角色列表,多个以逗号分隔,忽略空格 13 | */ 14 | public RemoteUserAdaptor(String unionid, String roles){ 15 | super(unionid, roles); 16 | } 17 | 18 | @Override 19 | public String toString() { 20 | return super.toString(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/realm/bean/CaptchaUserAdaptor.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.realm.bean; 2 | 3 | /** 4 | * 验证码用户数据适配器 5 | * @author yangyuan 6 | * @date 2018年7月4日 7 | */ 8 | public class CaptchaUserAdaptor extends UserAdaptor{ 9 | 10 | /** 11 | * 构造方法 12 | * @param unionid 用户全局唯一id 13 | * @param roles 用户角色列表,多个以逗号分隔,忽略空格 14 | */ 15 | public CaptchaUserAdaptor(String unionid, String roles) { 16 | super(unionid, roles); 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return super.toString(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Package Files 2 | *.jar 3 | *.war 4 | *.ear 5 | /target/ 6 | /.settings 7 | .settings 8 | 9 | # Compiled class file 10 | *.class 11 | 12 | # Log file 13 | *.log 14 | 15 | # BlueJ files 16 | *.ctxt 17 | 18 | # Mobile Tools for Java (J2ME) 19 | .mtj.tmp/ 20 | 21 | # Package Files # 22 | *.jar 23 | *.war 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | target/classes/security.properties 32 | target/classes/META-INF/maven/org.yangyuan/security/pom.properties 33 | .settings/org.eclipse.core.resources.prefs 34 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/dao/RemoteSessionDao.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.dao; 2 | 3 | import org.yangyuan.security.bean.User; 4 | import org.yangyuan.security.config.ResourceManager; 5 | import org.yangyuan.security.core.common.SecurityToken; 6 | import org.yangyuan.security.dao.common.AuthSessionDao; 7 | 8 | /** 9 | * 第三方登录认证数据访问层实现 10 | * @author yangyuan 11 | * @date 2017年4月26日 12 | */ 13 | public class RemoteSessionDao implements AuthSessionDao{ 14 | @Override 15 | public User auth(SecurityToken token) { 16 | return ResourceManager.dao().getRemoteRealm().getUser(token); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/SingleConcurrentSubjectControl.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | import org.yangyuan.security.bean.User; 4 | import org.yangyuan.security.core.common.ConcurrentSubjectControl; 5 | import org.yangyuan.security.util.SecurityUtils; 6 | 7 | /** 8 | * 单端覆盖登陆并发安全认证主题控制实现 9 | *

同一个账号同一时刻只能在一个客户端登陆,如果之前在其他客户端登陆过,那么之前的登陆将失效

10 | * @author yangyuan 11 | * @date 2018年4月25日 12 | */ 13 | public class SingleConcurrentSubjectControl implements ConcurrentSubjectControl{ 14 | 15 | @Override 16 | public void control(User user) { 17 | SecurityUtils.logout(user.getUnionid()); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/realm/common/JdbcRealmAdaptor.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.realm.common; 2 | 3 | import org.yangyuan.security.exception.common.AuthenticationBusinessException; 4 | import org.yangyuan.security.realm.bean.JdbcUser; 5 | import org.yangyuan.security.realm.bean.JdbcUserAdaptor; 6 | 7 | /** 8 | * 持久化数据源适配器定义 9 | * @author yangyuan 10 | * @date 2018年3月15日 11 | */ 12 | public interface JdbcRealmAdaptor { 13 | /** 14 | * 适配持久化用户数据 15 | * @param user 持久化用户数据 16 | * @return 持久化用户数据适配器 17 | * @throws AuthenticationBusinessException 18 | */ 19 | JdbcUserAdaptor selectByJdbcUser(JdbcUser user) throws AuthenticationBusinessException; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/realm/common/CaptchaRealmAdaptor.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.realm.common; 2 | 3 | import org.yangyuan.security.exception.common.AuthenticationBusinessException; 4 | import org.yangyuan.security.realm.bean.CaptchaUser; 5 | import org.yangyuan.security.realm.bean.CaptchaUserAdaptor; 6 | 7 | /** 8 | * 验证码数据源适配器定义 9 | * @author yangyuan 10 | * @date 2018年7月4日 11 | */ 12 | public interface CaptchaRealmAdaptor { 13 | /** 14 | * 适配验证码用户数据 15 | * @param user 验证码用户数据 16 | * @return 验证码用户数据适配器 17 | * @throws AuthenticationBusinessException 18 | */ 19 | CaptchaUserAdaptor selectByCaptchaUser(CaptchaUser user) throws AuthenticationBusinessException; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/realm/common/RemoteRealmAdaptor.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.realm.common; 2 | 3 | import org.yangyuan.security.exception.common.AuthenticationBusinessException; 4 | import org.yangyuan.security.realm.bean.RemoteUser; 5 | import org.yangyuan.security.realm.bean.RemoteUserAdaptor; 6 | 7 | /** 8 | * 第三方授权数据源适配器定义 9 | * @author yangyuan 10 | * @date 2018年3月15日 11 | */ 12 | public interface RemoteRealmAdaptor { 13 | /** 14 | * 适配第三方授权用户数据 15 | * @param user 第三方授权用户数据 16 | * @return 第三方授权用户数据适配器 17 | * @throws AuthenticationBusinessException 18 | */ 19 | RemoteUserAdaptor selectByRemoteUser(RemoteUser user) throws AuthenticationBusinessException; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/captcha/common/SecurityImageCaptcha.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.captcha.common; 2 | 3 | /** 4 | * 安全认证图形验证码定义 5 | * @author yangyuan 6 | * @date 2018年5月3日 7 | */ 8 | public interface SecurityImageCaptcha extends SecurityCaptcha{ 9 | 10 | /** 11 | * 创建一个新的唯一的令牌 12 | *

对于图形验证码而言,令牌是区分不同客户端的唯一标识

13 | * @return 唯一令牌 14 | */ 15 | String newToken(); 16 | 17 | /** 18 | * 获取指定令牌对应的验证码 19 | *

每个令牌对应唯一的验证码,验证码以图像的形式展示给用户

20 | * @param token 令牌,客户端标识 21 | * @return 22 | * 验证码。 23 | *
24 | * 如果指定令牌未发送过任何验证码或已经失效,则会返回null 25 | */ 26 | String getCode(String token); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/common/Subject.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core.common; 2 | 3 | /** 4 | * 安全认证主题定义,非常核心的接口 5 | * @author yangyuan 6 | * @date 2017年4月26日 7 | * @param 主题承载session key类型 8 | * @param 主题承载session value类型 9 | */ 10 | public interface Subject extends SecuritySerializable{ 11 | 12 | /** 13 | * 获取当前主题关联的唯一标识 14 | *

每个标识代表一个用户

15 | *

多个标识可能指向同一个用户

16 | * @return 唯一标识 17 | */ 18 | String getPrincipal(); 19 | 20 | /** 21 | * 获取当前主题关联的session实例 22 | * @return session实例 23 | */ 24 | Session getSession(); 25 | 26 | /** 27 | * 主题是否有效 28 | * @return true 有效,false 无效 29 | */ 30 | boolean isValid(); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/common/PrincipalFactory.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core.common; 2 | 3 | /** 4 | * 安全唯一标识生成器定义 5 | * @author yangyuan 6 | * @date 2017年4月26日 7 | */ 8 | public interface PrincipalFactory { 9 | 10 | /** 11 | * 创建安全唯一标识 12 | * @param userUnionid 绑定的用户唯一标识 13 | * @return 随机安全唯一标识 14 | */ 15 | String newPrincipal(String userUnionid); 16 | 17 | /** 18 | * 获取安全唯一标识中绑定的用户唯一标识 19 | * @param principal 安全唯一标识 20 | * @return 用户唯一标识 21 | */ 22 | String getUserUnionid(String principal); 23 | 24 | /** 25 | * 获取安全唯一标识所属分区 26 | *
27 | * 此方法一般用于底层数据存储分区优化,避免数据堆积 28 | * @param principal 安全唯一标识 29 | * @return 分区 30 | */ 31 | String getPartition(String principal); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/SessionManager.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | import org.yangyuan.security.core.common.Subject; 4 | 5 | /** 6 | * 会话管理器 7 | * @author yangyuan 8 | * @date 2017年4月26日 9 | */ 10 | public class SessionManager{ 11 | protected static final ThreadLocal> LOCAL_SUBJECT = new ThreadLocal>(); 12 | 13 | /** 14 | * 获取当前线程中的主题 15 | * @return 主题 16 | */ 17 | @SuppressWarnings("unchecked") 18 | public static Subject getSubject() { 19 | return (Subject) LOCAL_SUBJECT.get(); 20 | } 21 | 22 | /** 23 | * 设置当前线程中的主题 24 | * @param subject 主题 25 | */ 26 | public static void setSubject(Subject subject) { 27 | LOCAL_SUBJECT.set(subject); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/DefaultPrincipalFactory.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | import org.yangyuan.security.core.common.PrincipalFactory; 4 | 5 | /** 6 | * 默认安全唯一标识生成器实现 7 | * @author yangyuan 8 | * @date 2017年4月26日 9 | */ 10 | public class DefaultPrincipalFactory implements PrincipalFactory{ 11 | 12 | @Override 13 | public String newPrincipal(String userUnionid) { 14 | return userUnionid + "_" + System.nanoTime(); 15 | } 16 | 17 | @Override 18 | public String getUserUnionid(String principal) { 19 | return principal.split("_")[0]; 20 | } 21 | 22 | @Override 23 | public String getPartition(String principal) { 24 | String userUnionid = getUserUnionid(principal); 25 | 26 | return userUnionid.substring(0, 1); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/RefuseConcurrentSubjectControl.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | import org.yangyuan.security.bean.User; 4 | import org.yangyuan.security.core.common.ConcurrentSubjectControl; 5 | import org.yangyuan.security.exception.ConcurrentSubjectControlException; 6 | import org.yangyuan.security.util.SecurityUtils; 7 | 8 | /** 9 | * 单端独占登陆并发安全认证主题控制实现 10 | *

同一个账号同一时刻只能在一个客户端登陆,如果之前在其他客户端登陆过,那么本次登陆将会失败,除非其他客户端主动退出登陆

11 | * @author yangyuan 12 | * @date 2018年4月25日 13 | */ 14 | public class RefuseConcurrentSubjectControl implements ConcurrentSubjectControl{ 15 | 16 | @Override 17 | public void control(User user) { 18 | if(SecurityUtils.getUserSubjects(user.getUnionid()).size() > 0){ 19 | throw new ConcurrentSubjectControlException(); 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/PrincipalInvalidCookie.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.yangyuan.security.config.ResourceManager; 5 | import org.yangyuan.security.core.common.AbstractCookie; 6 | 7 | /** 8 | * 无效的主cookie实现 9 | * @author yangyuan 10 | * @date 2018年4月3日 11 | */ 12 | public class PrincipalInvalidCookie extends AbstractCookie{ 13 | 14 | @Override 15 | protected String getName() { 16 | return ResourceManager.cookie().getName(); 17 | } 18 | 19 | @Override 20 | protected String getValue() { 21 | return StringUtils.EMPTY; 22 | } 23 | 24 | @Override 25 | protected int getMaxAge() { 26 | return 0; 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return super.toString(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/realm/bean/JdbcUser.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.realm.bean; 2 | 3 | /** 4 | * 持久化用户数据 5 | * @author yangyuan 6 | * @date 2018年5月31日 7 | */ 8 | public class JdbcUser { 9 | 10 | /** 11 | * 构造方法 12 | * @param username 登陆账号 13 | */ 14 | public JdbcUser(String username){ 15 | this.username = username; 16 | } 17 | 18 | /** 19 | * 登陆账号 20 | */ 21 | private final String username; 22 | 23 | public String getUsername() { 24 | return username; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | StringBuilder builder = new StringBuilder(128); 30 | 31 | builder.append("[username]("); 32 | builder.append(getUsername()); 33 | builder.append(")\n"); 34 | 35 | return new String(builder); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/QqRemoteToken.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | /** 4 | * qq第三方登录令牌 5 | * @author yangyuan 6 | * @date 2018年4月3日 7 | */ 8 | public class QqRemoteToken extends RemoteToken{ 9 | 10 | /** 11 | * remember固定为true的构造方法 12 | * @param accessToken 第三方令牌 13 | */ 14 | public QqRemoteToken(String accessToken) { 15 | super(accessToken, true); 16 | } 17 | 18 | /** 19 | * 自定义remember的构造方法 20 | * @param accessToken 第三方令牌 21 | * @param remember 记住我 22 | */ 23 | public QqRemoteToken(String accessToken, boolean remember) { 24 | super(accessToken, remember); 25 | } 26 | 27 | @Override 28 | public int getPlanform() { 29 | return 1; 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return super.toString(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/PrincipalPersistentCookie.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | import org.yangyuan.security.config.ResourceManager; 4 | import org.yangyuan.security.core.common.AbstractCookie; 5 | 6 | /** 7 | * 持久的主cookie实现 8 | * @author yangyuan 9 | * @date 2018年4月3日 10 | */ 11 | public class PrincipalPersistentCookie extends AbstractCookie{ 12 | private final String principal; 13 | 14 | public PrincipalPersistentCookie(String principal){ 15 | this.principal = principal; 16 | } 17 | 18 | @Override 19 | protected String getName() { 20 | return ResourceManager.cookie().getName(); 21 | } 22 | 23 | @Override 24 | protected String getValue() { 25 | return this.principal; 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | return super.toString(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/WbRemoteToken.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | /** 4 | * 微博第三方登录令牌 5 | * @author yangyuan 6 | * @date 2018年4月3日 7 | */ 8 | public class WbRemoteToken extends RemoteToken{ 9 | 10 | /** 11 | * remember固定为true的构造方法 12 | * @param accessToken 第三方令牌 13 | */ 14 | public WbRemoteToken(String accessToken) { 15 | super(accessToken, true); 16 | } 17 | 18 | /** 19 | * 自定义remember的构造方法 20 | * @param accessToken 第三方令牌 21 | * @param remember 记住我 22 | */ 23 | public WbRemoteToken(String accessToken, boolean remember) { 24 | super(accessToken, remember); 25 | } 26 | 27 | @Override 28 | public int getPlanform() { 29 | return 3; 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return super.toString(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/exception/common/FilterException.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.exception.common; 2 | 3 | /** 4 | * 安全过滤器异常 5 | * @author yangyuan 6 | * @date 2018年4月27日 7 | */ 8 | public class FilterException extends RuntimeException{ 9 | private static final long serialVersionUID = -3599190239223935230L; 10 | 11 | public FilterException() { 12 | super(); 13 | } 14 | public FilterException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 15 | super(message, cause, enableSuppression, writableStackTrace); 16 | } 17 | public FilterException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | public FilterException(String message) { 21 | super(message); 22 | } 23 | public FilterException(Throwable cause) { 24 | super(cause); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/exception/common/CaptchaException.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.exception.common; 2 | 3 | /** 4 | * 验证码相关异常 5 | * @author yangyuan 6 | * @date 2018年5月4日 7 | */ 8 | public class CaptchaException extends RuntimeException{ 9 | private static final long serialVersionUID = 9211333410802654975L; 10 | 11 | public CaptchaException() { 12 | super(); 13 | } 14 | 15 | public CaptchaException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 16 | super(message, cause, enableSuppression, writableStackTrace); 17 | } 18 | 19 | public CaptchaException(String message, Throwable cause) { 20 | super(message, cause); 21 | } 22 | 23 | public CaptchaException(String message) { 24 | super(message); 25 | } 26 | 27 | public CaptchaException(Throwable cause) { 28 | super(cause); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/config/proxy/RedisResourceFactoryProxy.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.config.proxy; 2 | 3 | import org.yangyuan.security.dao.common.RedisResourceFactory; 4 | 5 | import redis.clients.jedis.Jedis; 6 | 7 | /** 8 | * redis连接工厂代理 9 | * @author yangyuan 10 | * @date 2018年6月20日 11 | */ 12 | public class RedisResourceFactoryProxy implements RedisResourceFactory{ 13 | 14 | private final RedisResourceFactory redisResourceFactory; 15 | 16 | public RedisResourceFactoryProxy(RedisResourceFactory redisResourceFactory){ 17 | this.redisResourceFactory = redisResourceFactory; 18 | } 19 | 20 | @Override 21 | public Jedis getResource() { 22 | if(redisResourceFactory == null){ 23 | throw new SecurityException("Load security.properties[common.redisResourceFactory] failed!"); 24 | } 25 | 26 | return redisResourceFactory.getResource(); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/realm/common/AbstractRealm.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.realm.common; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.apache.commons.lang3.StringUtils; 7 | import org.yangyuan.security.bean.Role; 8 | import org.yangyuan.security.bean.User; 9 | 10 | /** 11 | * 数据源抽象实现 12 | * @author yangyuan 13 | * @date 2018年3月15日 14 | */ 15 | public abstract class AbstractRealm implements Realm{ 16 | 17 | /** 18 | * 构造认证系统用户 19 | * @param unionid 用户唯一id 20 | * @param roles 字符串表示的用户角色列表,多个角色逗号分隔 21 | * @return 认证系统用户,实例中必须包含[用户全局唯一id(unionid)]和[用户角色列表(roles)] 22 | */ 23 | protected User getUser(String unionid, String roles){ 24 | List _roles = new ArrayList(); 25 | if(StringUtils.isNotBlank(roles)){ 26 | _roles = Role.parseRoles(roles); 27 | } 28 | 29 | return new User(unionid, _roles); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/common/AbstractSecurityToken.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core.common; 2 | 3 | /** 4 | * 抽象认证令牌实现 5 | * @author yangyuan 6 | * @date 2018年6月7日 7 | */ 8 | public abstract class AbstractSecurityToken implements SecurityToken{ 9 | /** 10 | * 记住我 11 | */ 12 | private final boolean remember; 13 | 14 | /** 15 | * 构造方法 16 | * @param remember 记住我 17 | */ 18 | public AbstractSecurityToken(boolean remember){ 19 | this.remember = remember; 20 | } 21 | 22 | @Override 23 | public boolean isRemember() { 24 | return this.remember; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | StringBuilder builder = new StringBuilder(128); 30 | 31 | builder.append("[remember]("); 32 | builder.append(isRemember()); 33 | builder.append(")\n"); 34 | 35 | return new String(builder); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 3 | 本框架基于`Spring MVC`开发,是一款轻量级的安全认证框架。 4 | 5 | 抛弃`Shiro`、`Spring Security`等安全框架繁琐的配置,改为注解实现权限管理,配合`Spring MVC`的`RequestMapping`注解,完美实现细粒度的权限控制。 6 | 7 | 本框架以`Redis`作为持久化数据库,`Ehcache`作为内存级缓存,满足高性能需求。 8 | 9 | 本框架删繁就简,以角色作为权限认证的唯一标准,并非传统的`RBAC`权限模型,在这里没有权限的概念,只有角色,角色就是权限,权限就是角色,因此本框架适合应用于互联网项目,尤其适合前后端分离模式下的后端接口。 10 | 11 | 本框架只关注角色认证,而不关注角色的存储、定义,彻底实现安全认证框架与实际项目之间的解耦。 12 | 13 | ## 特性 14 | 15 | * 高性能(设计简洁、内置缓存) 16 | * 基于注解 17 | * 安全的密码加密机制 18 | * 灵活的配置项 19 | * 易于集成、扩展 20 | * Session共享 21 | * 分布式部署 22 | * 实现*匿名认证*、*基础的登陆认证*、*基于角色的权限管理*、*基于范围表达式的权限管理*、*HTTP Basic Authentication* 23 | * 并发登录控制 24 | * 基础的在线会话管理 25 | * 验证码框架封装 26 | * 第三方登录集成 27 | 28 | ## 主要依赖 29 | 30 | * Spring MVC,基础依赖 31 | * Httpclient,第三方登陆依赖 32 | * FastJson,序列化依赖 33 | * Ehcache,缓存依赖 34 | * Redis,持久化依赖 35 | 36 | ## 开始使用 37 | 38 | [详见Wiki](https://github.com/iyangyuan/security/wiki "Go to Wiki") 39 | 40 | ## 温馨提示 41 | 42 | 在定义角色名称时,不应该出现框架已经占用的关键字,包括:`[`、`]`、`{`、`}`、`>`、`<`、`,`、`:`,否则会引起冲突。 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/exception/common/AuthenticationException.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.exception.common; 2 | 3 | /** 4 | * 安全认证相关异常 5 | * @author yangyuan 6 | * @date 2018年4月27日 7 | */ 8 | public class AuthenticationException extends RuntimeException{ 9 | private static final long serialVersionUID = -8592640217397866095L; 10 | 11 | public AuthenticationException() { 12 | super(); 13 | } 14 | public AuthenticationException(String message, Throwable cause, boolean enableSuppression, 15 | boolean writableStackTrace) { 16 | super(message, cause, enableSuppression, writableStackTrace); 17 | } 18 | public AuthenticationException(String message, Throwable cause) { 19 | super(message, cause); 20 | } 21 | public AuthenticationException(String message) { 22 | super(message); 23 | } 24 | public AuthenticationException(Throwable cause) { 25 | super(cause); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/PrincipalSessionCookie.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | import org.yangyuan.security.config.ResourceManager; 4 | import org.yangyuan.security.core.common.AbstractCookie; 5 | 6 | /** 7 | * 基于会话的主cookie实现 8 | * @author yangyuan 9 | * @date 2018年3月5日 10 | */ 11 | public class PrincipalSessionCookie extends AbstractCookie{ 12 | 13 | private final String principal; 14 | 15 | public PrincipalSessionCookie(String principal){ 16 | this.principal = principal; 17 | } 18 | 19 | @Override 20 | protected String getName() { 21 | return ResourceManager.cookie().getName(); 22 | } 23 | 24 | @Override 25 | protected String getValue() { 26 | return this.principal; 27 | } 28 | 29 | @Override 30 | protected int getMaxAge() { 31 | return -1; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return super.toString(); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/JdbcToken.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | import org.yangyuan.security.core.common.AbstractSecurityToken; 4 | 5 | /** 6 | * 本地登陆令牌 7 | * @author yangyuan 8 | * @date 2018年7月4日 9 | */ 10 | public abstract class JdbcToken extends AbstractSecurityToken{ 11 | /** 12 | * 用户名 13 | */ 14 | private final String username; 15 | 16 | public JdbcToken(String username, boolean remember) { 17 | super(remember); 18 | this.username = username; 19 | } 20 | 21 | public String getUsername() { 22 | return username; 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | StringBuilder builder = new StringBuilder(128); 28 | 29 | builder.append(super.toString()); 30 | 31 | builder.append("[username]("); 32 | builder.append(getUsername()); 33 | builder.append(")\n"); 34 | 35 | return new String(builder); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/exception/common/SecurityException.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.exception.common; 2 | 3 | /** 4 | * 安全认证通用异常 5 | *

此异常一般由底层方法抛出,用来泛指安全框架内部的异常

6 | * @author yangyuan 7 | * @date 2018年6月14日 8 | */ 9 | public class SecurityException extends RuntimeException{ 10 | private static final long serialVersionUID = -1375582426321088142L; 11 | 12 | public SecurityException() { 13 | super(); 14 | } 15 | 16 | public SecurityException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 17 | super(message, cause, enableSuppression, writableStackTrace); 18 | } 19 | 20 | public SecurityException(String message, Throwable cause) { 21 | super(message, cause); 22 | } 23 | 24 | public SecurityException(String message) { 25 | super(message); 26 | } 27 | 28 | public SecurityException(Throwable cause) { 29 | super(cause); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/filter/AnonSecurityFilter.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.filter; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | 5 | import org.yangyuan.security.bean.Permission; 6 | import org.yangyuan.security.core.annotation.SecurityFilterComponent; 7 | import org.yangyuan.security.exception.common.FilterException; 8 | import org.yangyuan.security.filter.common.SecurityFilter; 9 | 10 | /** 11 | * 匿名认证实现 12 | * @author yangyuan 13 | * @date 2017年4月26日 14 | */ 15 | @SecurityFilterComponent(value = "index/1") 16 | public class AnonSecurityFilter implements SecurityFilter{ 17 | 18 | private static final String FILETER_NAME = "anon"; 19 | 20 | @Override 21 | public boolean approve(Permission permission) { 22 | return permission.getName().toLowerCase().equals(FILETER_NAME); 23 | } 24 | 25 | @Override 26 | public void doFilter(Permission permission, HttpServletRequest request) throws FilterException{ 27 | return; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/common/Session.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core.common; 2 | 3 | /** 4 | * 会话定义 5 | * @author yangyuan 6 | * @date 2017年4月26日 7 | * @param key 类型 8 | * @param value 类型 9 | */ 10 | public interface Session extends SecuritySerializable{ 11 | 12 | /** 13 | * 获取会话数据 14 | * @param key the key whose associated value is to be returned 15 | * @return the value to which the specified key is mapped, or null if this map contains no mapping for the key 16 | */ 17 | V get(K key); 18 | 19 | /** 20 | * 设置会话数据 21 | * @param key key with which the specified value is to be associated 22 | * @param value value to be associated with the specified key 23 | * @return the previous value associated with key, or null if there was no mapping for key. (A null return can also indicate that the map previously associated null with key, if the implementation supports null values.) 24 | */ 25 | V set(K key, V value); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/filter/common/SecurityFilter.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.filter.common; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | 5 | import org.yangyuan.security.bean.Permission; 6 | import org.yangyuan.security.exception.common.FilterException; 7 | 8 | /** 9 | * 安全过滤器定义 10 | * @author yangyuan 11 | * @date 2017年4月26日 12 | */ 13 | public interface SecurityFilter { 14 | 15 | /** 16 | * 认证试探 17 | * @param permission 认证表达式 18 | * @return 19 | * true 此过滤器能够处理指定的认证表达式 20 | *
21 | * false 此过滤器不能够处理指定的认证表达式 22 | */ 23 | boolean approve(Permission permission); 24 | 25 | /** 26 | * 认证 27 | *

正常执行,认证通过

28 | *

认证失败会抛出对应的异常,通过异常机制实现认证交互

29 | * @param permission 认证表达式 30 | * @param request http请求对象 31 | * @throws FilterException 安全过滤器相关异常,具体含义参考子类定义 32 | */ 33 | void doFilter(Permission permission, HttpServletRequest request) throws FilterException; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/exception/AuthRemoteFailException.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.exception; 2 | 3 | import org.yangyuan.security.exception.common.AuthenticationException; 4 | 5 | /** 6 | * 第三方授权失败异常 7 | * @author yangyuan 8 | * @date 2018年3月16日 9 | */ 10 | public class AuthRemoteFailException extends AuthenticationException{ 11 | private static final long serialVersionUID = -2498726338813641428L; 12 | 13 | public AuthRemoteFailException() { 14 | super(); 15 | } 16 | 17 | public AuthRemoteFailException(String message) { 18 | super(message); 19 | } 20 | 21 | public AuthRemoteFailException(String message, Throwable cause, boolean enableSuppression, 22 | boolean writableStackTrace) { 23 | super(message, cause, enableSuppression, writableStackTrace); 24 | } 25 | 26 | public AuthRemoteFailException(String message, Throwable cause) { 27 | super(message, cause); 28 | } 29 | 30 | public AuthRemoteFailException(Throwable cause) { 31 | super(cause); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/exception/SecurityFilterAuthException.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.exception; 2 | 3 | import org.yangyuan.security.exception.common.FilterException; 4 | 5 | /** 6 | * 基础认证失败异常 7 | * @author yangyuan 8 | * @date 2017年4月26日 9 | */ 10 | public class SecurityFilterAuthException extends FilterException{ 11 | private static final long serialVersionUID = 4317541537006535404L; 12 | 13 | public SecurityFilterAuthException() { 14 | super(); 15 | } 16 | 17 | public SecurityFilterAuthException(String s) { 18 | super(s); 19 | } 20 | 21 | public SecurityFilterAuthException(String message, Throwable cause, boolean enableSuppression, 22 | boolean writableStackTrace) { 23 | super(message, cause, enableSuppression, writableStackTrace); 24 | } 25 | 26 | public SecurityFilterAuthException(String message, Throwable cause) { 27 | super(message, cause); 28 | } 29 | 30 | public SecurityFilterAuthException(Throwable cause) { 31 | super(cause); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/exception/CaptchaSendTooFastException.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.exception; 2 | 3 | import org.yangyuan.security.exception.common.CaptchaException; 4 | 5 | /** 6 | * 验证码发送频率过快异常 7 | * @author yangyuan 8 | * @date 2018年5月4日 9 | */ 10 | public class CaptchaSendTooFastException extends CaptchaException{ 11 | private static final long serialVersionUID = 2287247809868089731L; 12 | 13 | public CaptchaSendTooFastException() { 14 | super(); 15 | } 16 | 17 | public CaptchaSendTooFastException(String message, Throwable cause, boolean enableSuppression, 18 | boolean writableStackTrace) { 19 | super(message, cause, enableSuppression, writableStackTrace); 20 | } 21 | 22 | public CaptchaSendTooFastException(String message, Throwable cause) { 23 | super(message, cause); 24 | } 25 | 26 | public CaptchaSendTooFastException(String message) { 27 | super(message); 28 | } 29 | 30 | public CaptchaSendTooFastException(Throwable cause) { 31 | super(cause); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/dao/common/CacheSessionDao.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.dao.common; 2 | 3 | import java.util.List; 4 | 5 | import org.yangyuan.security.core.common.Subject; 6 | 7 | /** 8 | * 缓存数据访问层定义 9 | * @author yangyuan 10 | * @date 2017年4月26日 11 | * @param key 类型 12 | * @param value 类型 13 | */ 14 | public interface CacheSessionDao { 15 | 16 | /** 17 | * 读取主题 18 | * @param subject 主题 19 | * @return 缓存中的主题 20 | */ 21 | Subject doRead(Subject subject); 22 | 23 | /** 24 | * 创建主题 25 | * @param subject 主题 26 | */ 27 | void doCreate(Subject subject); 28 | 29 | /** 30 | * 移除主题 31 | * @param subject 主题 32 | */ 33 | void doDelete(Subject subject); 34 | 35 | /** 36 | * 查询用户主题列表 37 | *
38 | * 对于多端登陆的场景,此方法必须返回用户所有端的主题 39 | *
40 | * 同一个用户可能对应多个主题 41 | * @param userUnionid 用户唯一标识 42 | * @return 用户主题列表 43 | */ 44 | List> queryUserSubjects(String userUnionid); 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/exception/AuthPasswordWrongException.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.exception; 2 | 3 | import org.yangyuan.security.exception.common.AuthenticationException; 4 | 5 | /** 6 | * 密码不正确异常 7 | * @author yangyuan 8 | * @date 2017年4月26日 9 | */ 10 | public class AuthPasswordWrongException extends AuthenticationException{ 11 | private static final long serialVersionUID = 4317541537006535402L; 12 | 13 | public AuthPasswordWrongException() { 14 | super(); 15 | } 16 | 17 | public AuthPasswordWrongException(String s) { 18 | super(s); 19 | } 20 | 21 | public AuthPasswordWrongException(String message, Throwable cause, boolean enableSuppression, 22 | boolean writableStackTrace) { 23 | super(message, cause, enableSuppression, writableStackTrace); 24 | } 25 | 26 | public AuthPasswordWrongException(String message, Throwable cause) { 27 | super(message, cause); 28 | } 29 | 30 | public AuthPasswordWrongException(Throwable cause) { 31 | super(cause); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/exception/SecurityFilterErrorException.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.exception; 2 | 3 | import org.yangyuan.security.exception.common.FilterException; 4 | 5 | /** 6 | * 认证过程出现严重bug,无法继续进行异常 7 | * @author yangyuan 8 | * @date 2017年4月26日 9 | */ 10 | public class SecurityFilterErrorException extends FilterException{ 11 | private static final long serialVersionUID = 4317541537006535405L; 12 | 13 | public SecurityFilterErrorException() { 14 | super(); 15 | } 16 | 17 | public SecurityFilterErrorException(String s) { 18 | super(s); 19 | } 20 | 21 | public SecurityFilterErrorException(String message, Throwable cause, boolean enableSuppression, 22 | boolean writableStackTrace) { 23 | super(message, cause, enableSuppression, writableStackTrace); 24 | } 25 | 26 | public SecurityFilterErrorException(String message, Throwable cause) { 27 | super(message, cause); 28 | } 29 | 30 | public SecurityFilterErrorException(Throwable cause) { 31 | super(cause); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/exception/AuthUserDisabledException.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.exception; 2 | 3 | import org.yangyuan.security.exception.common.AuthenticationBusinessException; 4 | 5 | /** 6 | * 用户不可用异常 7 | * @author yangyuan 8 | * @date 2018年6月4日 9 | */ 10 | public class AuthUserDisabledException extends AuthenticationBusinessException{ 11 | private static final long serialVersionUID = -7455615842195744230L; 12 | 13 | public AuthUserDisabledException() { 14 | super(); 15 | } 16 | 17 | public AuthUserDisabledException(String message, Throwable cause, boolean enableSuppression, 18 | boolean writableStackTrace) { 19 | super(message, cause, enableSuppression, writableStackTrace); 20 | } 21 | 22 | public AuthUserDisabledException(String message, Throwable cause) { 23 | super(message, cause); 24 | } 25 | 26 | public AuthUserDisabledException(String message) { 27 | super(message); 28 | } 29 | 30 | public AuthUserDisabledException(Throwable cause) { 31 | super(cause); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/exception/AuthUsernameNotFoundException.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.exception; 2 | 3 | import org.yangyuan.security.exception.common.AuthenticationException; 4 | 5 | /** 6 | * 用户不存在异常 7 | * @author yangyuan 8 | * @date 2017年4月26日 9 | */ 10 | public class AuthUsernameNotFoundException extends AuthenticationException{ 11 | private static final long serialVersionUID = 4317541537006535403L; 12 | 13 | public AuthUsernameNotFoundException() { 14 | super(); 15 | } 16 | 17 | public AuthUsernameNotFoundException(String s) { 18 | super(s); 19 | } 20 | 21 | public AuthUsernameNotFoundException(String message, Throwable cause, boolean enableSuppression, 22 | boolean writableStackTrace) { 23 | super(message, cause, enableSuppression, writableStackTrace); 24 | } 25 | 26 | public AuthUsernameNotFoundException(String message, Throwable cause) { 27 | super(message, cause); 28 | } 29 | 30 | public AuthUsernameNotFoundException(Throwable cause) { 31 | super(cause); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/exception/SecurityFilterForbiddenException.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.exception; 2 | 3 | import org.yangyuan.security.exception.common.FilterException; 4 | 5 | /** 6 | * 角色认证失败异常 7 | * @author yangyuan 8 | * @date 2017年4月26日 9 | */ 10 | public class SecurityFilterForbiddenException extends FilterException{ 11 | private static final long serialVersionUID = 8393729605818992367L; 12 | 13 | public SecurityFilterForbiddenException() { 14 | super(); 15 | } 16 | 17 | public SecurityFilterForbiddenException(String s) { 18 | super(s); 19 | } 20 | 21 | public SecurityFilterForbiddenException(String message, Throwable cause, boolean enableSuppression, 22 | boolean writableStackTrace) { 23 | super(message, cause, enableSuppression, writableStackTrace); 24 | } 25 | 26 | public SecurityFilterForbiddenException(String message, Throwable cause) { 27 | super(message, cause); 28 | } 29 | 30 | public SecurityFilterForbiddenException(Throwable cause) { 31 | super(cause); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/dao/JdbcSessionDao.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.dao; 2 | 3 | import org.yangyuan.security.bean.User; 4 | import org.yangyuan.security.config.ResourceManager; 5 | import org.yangyuan.security.core.CaptchaToken; 6 | import org.yangyuan.security.core.UsernamePasswordToken; 7 | import org.yangyuan.security.core.common.SecurityToken; 8 | import org.yangyuan.security.dao.common.AuthSessionDao; 9 | import org.yangyuan.security.exception.common.SecurityException; 10 | 11 | /** 12 | * 本地认证数据访问层实现 13 | * @author yangyuan 14 | * @date 2017年4月26日 15 | */ 16 | public class JdbcSessionDao implements AuthSessionDao{ 17 | 18 | @Override 19 | public User auth(SecurityToken token) { 20 | if(token instanceof UsernamePasswordToken){ 21 | return ResourceManager.dao().getJdbcRealm().getUser(token); 22 | } 23 | if(token instanceof CaptchaToken){ 24 | return ResourceManager.dao().getCaptchaRealm().getUser(token); 25 | } 26 | throw new SecurityException("Unknown token type[" + token.getClass().getName() + "]"); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/config/proxy/JdbcRealmAdaptorProxy.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.config.proxy; 2 | 3 | import org.yangyuan.security.exception.common.AuthenticationBusinessException; 4 | import org.yangyuan.security.realm.bean.JdbcUser; 5 | import org.yangyuan.security.realm.bean.JdbcUserAdaptor; 6 | import org.yangyuan.security.realm.common.JdbcRealmAdaptor; 7 | 8 | /** 9 | * 持久化数据源适配器代理 10 | * @author yangyuan 11 | * @date 2018年6月20日 12 | */ 13 | public class JdbcRealmAdaptorProxy implements JdbcRealmAdaptor{ 14 | 15 | private final JdbcRealmAdaptor jdbcRealmAdaptor; 16 | 17 | public JdbcRealmAdaptorProxy(JdbcRealmAdaptor jdbcRealmAdaptor){ 18 | this.jdbcRealmAdaptor = jdbcRealmAdaptor; 19 | } 20 | 21 | @Override 22 | public JdbcUserAdaptor selectByJdbcUser(JdbcUser user) throws AuthenticationBusinessException { 23 | if(jdbcRealmAdaptor == null){ 24 | throw new SecurityException("Load security.properties[dao.jdbcRealmAdaptor] failed!"); 25 | } 26 | 27 | return jdbcRealmAdaptor.selectByJdbcUser(user); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | security 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.wst.common.project.facet.core.builder 10 | 11 | 12 | 13 | 14 | org.eclipse.jdt.core.javabuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.m2e.core.maven2Builder 20 | 21 | 22 | 23 | 24 | org.eclipse.wst.validation.validationbuilder 25 | 26 | 27 | 28 | 29 | 30 | org.eclipse.jem.workbench.JavaEMFNature 31 | org.eclipse.wst.common.modulecore.ModuleCoreNature 32 | org.eclipse.jdt.core.javanature 33 | org.eclipse.m2e.core.maven2Nature 34 | org.eclipse.wst.common.project.facet.core.nature 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/exception/SecurityFilterBasicAuthException.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.exception; 2 | 3 | import org.yangyuan.security.exception.common.FilterException; 4 | 5 | /** 6 | * http basic authentication认证失败异常 7 | * @author yangyuan 8 | * @date 2018年4月12日 9 | */ 10 | public class SecurityFilterBasicAuthException extends FilterException{ 11 | private static final long serialVersionUID = 8393729605818992366L; 12 | 13 | public SecurityFilterBasicAuthException() { 14 | super(); 15 | } 16 | 17 | public SecurityFilterBasicAuthException(String s) { 18 | super(s); 19 | } 20 | 21 | public SecurityFilterBasicAuthException(String message, Throwable cause, boolean enableSuppression, 22 | boolean writableStackTrace) { 23 | super(message, cause, enableSuppression, writableStackTrace); 24 | } 25 | 26 | public SecurityFilterBasicAuthException(String message, Throwable cause) { 27 | super(message, cause); 28 | } 29 | 30 | public SecurityFilterBasicAuthException(Throwable cause) { 31 | super(cause); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/filter/common/AbstractSecurityCacheFilter.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.filter.common; 2 | 3 | import java.util.concurrent.ConcurrentHashMap; 4 | 5 | /** 6 | * 具有缓存的安全过滤器抽象实现 7 | *
8 | * 此类没有实现缓存淘汰算法,也没有任何缓存回收机制 9 | *
10 | * 所以此缓存存放的元素必须是可以预期的元素,即数量可控,容量可控 11 | * @author yangyuan 12 | * @date 2018年4月19日 13 | */ 14 | public abstract class AbstractSecurityCacheFilter implements SecurityFilter{ 15 | /** 16 | * 缓存容器 17 | */ 18 | private final ConcurrentHashMap cache = new ConcurrentHashMap(); 19 | 20 | /** 21 | * 缓存 22 | *
23 | * 线程安全 24 | * @param key 键 25 | * @param value 值 26 | */ 27 | protected void caching(String key, Object value){ 28 | cache.putIfAbsent(key, value); 29 | } 30 | 31 | /** 32 | * 获取指定键对应的缓存值 33 | *
34 | * 线程安全 35 | * @param key 键 36 | * @return 缓存未命中返回null,否则返回缓存的值 37 | */ 38 | @SuppressWarnings("unchecked") 39 | protected T cached(String key){ 40 | return (T) cache.get(key); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/exception/ConcurrentSubjectControlException.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.exception; 2 | 3 | import org.yangyuan.security.exception.common.AuthenticationException; 4 | 5 | /** 6 | * 并发安全认证主题控制异常 7 | * @author yangyuan 8 | * @date 2018年4月25日 9 | */ 10 | public class ConcurrentSubjectControlException extends AuthenticationException{ 11 | private static final long serialVersionUID = 6681662640973349551L; 12 | 13 | public ConcurrentSubjectControlException() { 14 | super(); 15 | } 16 | 17 | public ConcurrentSubjectControlException(String message, Throwable cause, boolean enableSuppression, 18 | boolean writableStackTrace) { 19 | super(message, cause, enableSuppression, writableStackTrace); 20 | } 21 | 22 | public ConcurrentSubjectControlException(String message, Throwable cause) { 23 | super(message, cause); 24 | } 25 | 26 | public ConcurrentSubjectControlException(String message) { 27 | super(message); 28 | } 29 | 30 | public ConcurrentSubjectControlException(Throwable cause) { 31 | super(cause); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/realm/bean/JdbcUserAdaptor.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.realm.bean; 2 | 3 | /** 4 | * 持久化用户数据适配器 5 | * @author yangyuan 6 | * @date 2018年5月31日 7 | */ 8 | public class JdbcUserAdaptor extends UserAdaptor{ 9 | 10 | /** 11 | * 构造方法 12 | * @param unionid 用户全局唯一id 13 | * @param roles 用户角色列表,多个以逗号分隔,忽略空格 14 | * @param password 登陆账号对应的密码 15 | */ 16 | public JdbcUserAdaptor(String unionid, String roles, String password){ 17 | super(unionid, roles); 18 | this.password = password; 19 | } 20 | 21 | /** 22 | * 登陆账号对应的密码 23 | */ 24 | private final String password; 25 | 26 | public String getPassword() { 27 | return password; 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | StringBuilder builder = new StringBuilder(128); 33 | 34 | builder.append(super.toString()); 35 | 36 | builder.append("[password]("); 37 | builder.append(getPassword()); 38 | builder.append(")\n"); 39 | 40 | return new String(builder); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/realm/bean/CaptchaUser.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.realm.bean; 2 | 3 | /** 4 | * 验证码用户数据 5 | * @author yangyuan 6 | * @date 2018年7月4日 7 | */ 8 | public class CaptchaUser { 9 | /** 10 | * 用户名 11 | */ 12 | private final String username; 13 | 14 | /** 15 | * 验证码 16 | */ 17 | private final String code; 18 | 19 | public CaptchaUser(String username, String code){ 20 | this.username = username; 21 | this.code = code; 22 | } 23 | 24 | public String getUsername() { 25 | return username; 26 | } 27 | 28 | public String getCode() { 29 | return code; 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | StringBuilder builder = new StringBuilder(128); 35 | 36 | builder.append("[username]("); 37 | builder.append(getUsername()); 38 | builder.append(")\n"); 39 | 40 | builder.append("[code]("); 41 | builder.append(getCode()); 42 | builder.append(")\n"); 43 | 44 | return new String(builder); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/annotation/SecurityFilterComponent.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * 安全认证过滤器注解 12 | *
13 | * 任何过滤器实现类,只需配置此注解即可生效 14 | *
15 | * 此注解的value值可以用来对过滤器进行排序,非必填,如果不指定value值,顺序随机,指定了value值的过滤器,一定排在未指定value值的过滤器之前 16 | *

排序表达式说明:

17 | *
18 | * index/?,“?”代表排序序号,例如:index/123 19 | *
20 | * 排序序号为大于100的整数,不可重复,序号数值越大过滤器越靠后 21 | * @author yangyuan 22 | * @date 2018年4月13日 23 | */ 24 | @Target({ElementType.TYPE}) 25 | @Retention(RetentionPolicy.RUNTIME) 26 | @Component 27 | public @interface SecurityFilterComponent { 28 | /** 29 | * The value may indicate a suggestion for a logical component name, 30 | * to be turned into a Spring bean in case of an autodetected component. 31 | * @return the suggested component name, if any 32 | */ 33 | String value() default ""; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/util/SecurityHexUtil.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.util; 2 | 3 | /** 4 | * 十六进制工具类 5 | * @author yangyuan 6 | * @date 2018年4月8日 7 | */ 8 | public class SecurityHexUtil { 9 | private final static String[] HEX_DIGITS = {"0", "1", "2", "3", "4", "5", "6", "7", 10 | "8", "9", "a", "b", "c", "d", "e", "f"}; 11 | 12 | /** 13 | * 转换字节数组为十六进制字符串 14 | * @param bytes 字节数组 15 | * @return 十六进制字符串 16 | */ 17 | public static String byteArrayToHexString(byte[] bytes) { 18 | StringBuilder builder = new StringBuilder(64); 19 | for(byte b : bytes) { 20 | builder.append(byteToHexString(b)); 21 | } 22 | 23 | return builder.toString(); 24 | } 25 | 26 | /** 27 | * 转换byte到十六进制 28 | * @param b 要转换的byte 29 | * @return 十六进制字符串 30 | */ 31 | private static String byteToHexString(byte b) { 32 | int n = b; 33 | if(n < 0) { 34 | n = 256 + n; 35 | } 36 | int d1 = n / 16; 37 | int d2 = n % 16; 38 | 39 | return HEX_DIGITS[d1] + HEX_DIGITS[d2]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/config/proxy/RemoteRealmAdaptorProxy.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.config.proxy; 2 | 3 | import org.yangyuan.security.exception.common.AuthenticationBusinessException; 4 | import org.yangyuan.security.realm.bean.RemoteUser; 5 | import org.yangyuan.security.realm.bean.RemoteUserAdaptor; 6 | import org.yangyuan.security.realm.common.RemoteRealmAdaptor; 7 | 8 | /** 9 | * 第三方授权数据源适配器代理 10 | * @author yangyuan 11 | * @date 2018年6月20日 12 | */ 13 | public class RemoteRealmAdaptorProxy implements RemoteRealmAdaptor{ 14 | 15 | private final RemoteRealmAdaptor remoteRealmAdaptor; 16 | 17 | public RemoteRealmAdaptorProxy(RemoteRealmAdaptor remoteRealmAdaptor){ 18 | this.remoteRealmAdaptor = remoteRealmAdaptor; 19 | } 20 | 21 | @Override 22 | public RemoteUserAdaptor selectByRemoteUser(RemoteUser user) throws AuthenticationBusinessException { 23 | if(remoteRealmAdaptor == null){ 24 | throw new SecurityException("Load security.properties[dao.remoteRealmAdaptor] failed!"); 25 | } 26 | 27 | return remoteRealmAdaptor.selectByRemoteUser(user); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/exception/CaptchaUnknownAccountTypeException.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.exception; 2 | 3 | import org.yangyuan.security.exception.common.CaptchaException; 4 | 5 | /** 6 | * 发送验证码时发现未知的账号类型 7 | *

目前仅支持手机号、邮箱

8 | * @author yangyuan 9 | * @date 2018年5月4日 10 | */ 11 | public class CaptchaUnknownAccountTypeException extends CaptchaException{ 12 | private static final long serialVersionUID = 4324314748795356440L; 13 | 14 | public CaptchaUnknownAccountTypeException() { 15 | super(); 16 | } 17 | 18 | public CaptchaUnknownAccountTypeException(String message, Throwable cause, boolean enableSuppression, 19 | boolean writableStackTrace) { 20 | super(message, cause, enableSuppression, writableStackTrace); 21 | } 22 | 23 | public CaptchaUnknownAccountTypeException(String message, Throwable cause) { 24 | super(message, cause); 25 | } 26 | 27 | public CaptchaUnknownAccountTypeException(String message) { 28 | super(message); 29 | } 30 | 31 | public CaptchaUnknownAccountTypeException(Throwable cause) { 32 | super(cause); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/exception/common/AuthenticationBusinessException.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.exception.common; 2 | 3 | /** 4 | * 业务逻辑相关异常定义 5 | *

此类异常实现业务逻辑与框架之间的交互,完全由框架使用者决定何时抛出

6 | *

使用者根据具体业务逻辑可以判定某些用户不符合认证条件,那么可以借助此异常提前终止认证

7 | * @author yangyuan 8 | * @date 2018年6月4日 9 | */ 10 | public class AuthenticationBusinessException extends AuthenticationException{ 11 | private static final long serialVersionUID = -949903305690611971L; 12 | 13 | public AuthenticationBusinessException() { 14 | super(); 15 | } 16 | 17 | public AuthenticationBusinessException(String message, Throwable cause, boolean enableSuppression, 18 | boolean writableStackTrace) { 19 | super(message, cause, enableSuppression, writableStackTrace); 20 | } 21 | 22 | public AuthenticationBusinessException(String message, Throwable cause) { 23 | super(message, cause); 24 | } 25 | 26 | public AuthenticationBusinessException(String message) { 27 | super(message); 28 | } 29 | 30 | public AuthenticationBusinessException(Throwable cause) { 31 | super(cause); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/config/proxy/CaptchaRealmAdaptorProxy.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.config.proxy; 2 | 3 | import org.yangyuan.security.exception.common.AuthenticationBusinessException; 4 | import org.yangyuan.security.realm.bean.CaptchaUser; 5 | import org.yangyuan.security.realm.bean.CaptchaUserAdaptor; 6 | import org.yangyuan.security.realm.common.CaptchaRealmAdaptor; 7 | 8 | /** 9 | * 验证码数据源适配器代理 10 | * @author yangyuan 11 | * @date 2018年7月4日 12 | */ 13 | public class CaptchaRealmAdaptorProxy implements CaptchaRealmAdaptor{ 14 | 15 | private final CaptchaRealmAdaptor captchaRealmAdaptor; 16 | 17 | public CaptchaRealmAdaptorProxy(CaptchaRealmAdaptor captchaRealmAdaptor){ 18 | this.captchaRealmAdaptor = captchaRealmAdaptor; 19 | } 20 | 21 | @Override 22 | public CaptchaUserAdaptor selectByCaptchaUser(CaptchaUser user) throws AuthenticationBusinessException { 23 | if(captchaRealmAdaptor == null){ 24 | throw new SecurityException("Load security.properties[dao.captchaRealmAdaptor] failed!"); 25 | } 26 | 27 | return captchaRealmAdaptor.selectByCaptchaUser(user); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/captcha/common/SecurityCaptcha.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.captcha.common; 2 | 3 | import org.yangyuan.security.exception.common.CaptchaException; 4 | 5 | /** 6 | * 安全认证验证码定义 7 | * @author yangyuan 8 | * @date 2018年5月3日 9 | */ 10 | public interface SecurityCaptcha { 11 | 12 | /** 13 | * 向指定账号发送验证码 14 | * @param account 账号 15 | * @throws CaptchaException 16 | */ 17 | void send(String account) throws CaptchaException; 18 | 19 | /** 20 | * 匹配指定账号对应的验证码 21 | * @param account 账号 22 | * @param code 验证码 23 | * @return true 如果给定的账号匹配给定的验证码
false 其他情况 24 | */ 25 | boolean match(String account, String code); 26 | 27 | /** 28 | * 移除指定账号对应的验证码 29 | * @param account 账号 30 | */ 31 | void remove(String account); 32 | 33 | /** 34 | * 生成一个新的验证码 35 | *

验证码生成策略交予框架使用者实现

36 | *

因为这是一个非常灵活的实现

37 | *

对于手机短信、邮箱邮件验证码,建议生成纯数字

38 | *

对于图形验证码,需要配合具体图形生成器实现。有些图形生成器支持汉字,那么此方法可以返回汉字;若不支持,那么只能返回字母或数字。

39 | *

理论上生成的验证码允许重复,但重复率不要过高

40 | * @param size 验证码长度 41 | * @return 验证码 42 | */ 43 | String newCode(int size); 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/http/client/common/HttpOptions.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.http.client.common; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import org.yangyuan.security.config.ResourceManager; 7 | 8 | /** 9 | * HTTP客户端通用请求配置 10 | * @author yangyuan 11 | * @date 2018年4月17日 12 | */ 13 | public abstract class HttpOptions { 14 | 15 | /** 16 | * 自定义请求头域 17 | *

不建议用此方法定义Content-Type域

18 | *

定义Content-Type域请用getContentType方法

19 | * @return 头域 20 | */ 21 | public Map getHeaders(){ 22 | return new HashMap(); 23 | } 24 | 25 | /** 26 | * 自定义请求体媒体类型 27 | *

本方法会被getHeaders方法中定义的Content-Type域覆盖

28 | * @return 媒体类型 29 | */ 30 | public String getContentType(){ 31 | return "text/plain; charset=utf-8"; 32 | } 33 | 34 | /** 35 | * 自定义请求体编码 36 | * @return 编码 37 | */ 38 | public String getRequestBodyCharset(){ 39 | return ResourceManager.core().getCharset(); 40 | } 41 | 42 | /** 43 | * 自定义响应体编码 44 | * @return 编码 45 | */ 46 | public String getResponseBodyCharset(){ 47 | return ResourceManager.core().getCharset(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/realm/bean/UserAdaptor.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.realm.bean; 2 | 3 | /** 4 | * 公共用户数据适配器 5 | * @author yangyuan 6 | * @date 2018年3月14日 7 | */ 8 | public class UserAdaptor { 9 | 10 | /** 11 | * 构造方法 12 | * @param unionid 用户全局唯一id 13 | * @param roles 用户角色列表,多个以逗号分隔,忽略空格 14 | */ 15 | public UserAdaptor(String unionid, String roles){ 16 | this.unionid = unionid; 17 | this.roles = roles; 18 | } 19 | 20 | /** 21 | * 用户全局唯一id 22 | */ 23 | private final String unionid; 24 | /** 25 | * 用户角色列表,多个以逗号分隔,忽略空格 26 | */ 27 | private final String roles; 28 | 29 | public String getUnionid() { 30 | return unionid; 31 | } 32 | 33 | public String getRoles() { 34 | return roles; 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | StringBuilder builder = new StringBuilder(128); 40 | 41 | builder.append("[unionid]("); 42 | builder.append(getUnionid()); 43 | builder.append(")\n"); 44 | 45 | builder.append("[roles]("); 46 | builder.append(getRoles()); 47 | builder.append(")\n"); 48 | 49 | return new String(builder); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/util/SecurityDate.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.util; 2 | 3 | import java.util.Calendar; 4 | import java.util.Date; 5 | 6 | /** 7 | * 时间工具类 8 | * @author yangyuan 9 | * @date 2018年4月8日 10 | */ 11 | public class SecurityDate { 12 | /** 13 | * 返回指定日期的结束时间 14 | * 15 | * @param date 16 | * 指定日期(例如2014-08-01) 17 | * @return 返回结束时间(例如2014-08-01 23:59:59) 18 | */ 19 | public static Date getIntegralEndTime(Date date) { 20 | return getResetTime(date, 23, 59, 59); 21 | } 22 | 23 | /** 24 | * 获取重置指定日期的时分秒后的时间 25 | * 26 | * @param date 27 | * 指定日期 28 | * @param hour 29 | * 指定小时 30 | * @param minute 31 | * 指定分钟 32 | * @param second 33 | * 指定秒 34 | * @return 返回重置时分秒后的时间 35 | */ 36 | public static Date getResetTime(Date date, int hour, int minute, int second) { 37 | Calendar cal = Calendar.getInstance(); 38 | if (date != null) { 39 | cal.setTime(date); 40 | } 41 | cal.set(Calendar.HOUR_OF_DAY, hour); 42 | cal.set(Calendar.SECOND, minute); 43 | cal.set(Calendar.MINUTE, second); 44 | return cal.getTime(); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/CaptchaToken.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | /** 4 | * 本地验证码模式令牌实现 5 | * @author yangyuan 6 | * @date 2018年7月4日 7 | */ 8 | public class CaptchaToken extends JdbcToken{ 9 | /** 10 | * 验证码 11 | */ 12 | private final String code; 13 | 14 | /** 15 | * remember固定为true的构造方法 16 | * @param username 用户名 17 | * @param code 验证码 18 | */ 19 | public CaptchaToken(String username, String code) { 20 | super(username, true); 21 | this.code = code; 22 | } 23 | 24 | /** 25 | * 自定义remember的构造方法 26 | * @param username 用户名 27 | * @param code 验证码 28 | * @param remember 记住我 29 | */ 30 | public CaptchaToken(String username, String code, boolean remember) { 31 | super(username, remember); 32 | this.code = code; 33 | } 34 | 35 | public String getCode() { 36 | return code; 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | StringBuilder builder = new StringBuilder(128); 42 | 43 | builder.append(super.toString()); 44 | 45 | builder.append("[code]("); 46 | builder.append(getCode()); 47 | builder.append(")\n"); 48 | 49 | return new String(builder); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/UsernamePasswordToken.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | /** 4 | * 本地用户名、密码模式令牌实现 5 | * @author yangyuan 6 | * @date 2017年4月26日 7 | */ 8 | public class UsernamePasswordToken extends JdbcToken { 9 | /** 10 | * 密码 11 | */ 12 | private final String passwrod; 13 | 14 | /** 15 | * remember固定为true的构造方法 16 | * @param username 用户名 17 | * @param passwrod 密码 18 | */ 19 | public UsernamePasswordToken(String username, String passwrod){ 20 | super(username, true); 21 | this.passwrod = passwrod; 22 | } 23 | 24 | /** 25 | * 自定义remember的构造方法 26 | * @param username 用户名 27 | * @param passwrod 密码 28 | * @param remember 记住我 29 | */ 30 | public UsernamePasswordToken(String username, String passwrod, boolean remember){ 31 | super(username, remember); 32 | this.passwrod = passwrod; 33 | } 34 | 35 | public String getPasswrod() { 36 | return passwrod; 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | StringBuilder builder = new StringBuilder(128); 42 | 43 | builder.append(super.toString()); 44 | 45 | builder.append("[passwrod]("); 46 | builder.append(getPasswrod()); 47 | builder.append(")\n"); 48 | 49 | return new String(builder); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/realm/captcha/CaptchaRealm.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.realm.captcha; 2 | 3 | import org.yangyuan.security.bean.User; 4 | import org.yangyuan.security.config.ResourceManager; 5 | import org.yangyuan.security.core.CaptchaToken; 6 | import org.yangyuan.security.core.common.SecurityToken; 7 | import org.yangyuan.security.exception.AuthUsernameNotFoundException; 8 | import org.yangyuan.security.realm.bean.CaptchaUser; 9 | import org.yangyuan.security.realm.bean.CaptchaUserAdaptor; 10 | import org.yangyuan.security.realm.common.AbstractRealm; 11 | 12 | /** 13 | * 验证码数据源实现 14 | * @author yangyuan 15 | * @date 2018年7月4日 16 | */ 17 | public class CaptchaRealm extends AbstractRealm{ 18 | 19 | @Override 20 | public User getUser(SecurityToken token) { 21 | CaptchaToken captchaToken = (CaptchaToken) token; 22 | 23 | /** 24 | * 适配数据 25 | */ 26 | CaptchaUser captchaUser = new CaptchaUser(captchaToken.getUsername(), captchaToken.getCode()); 27 | CaptchaUserAdaptor userAdaptor = ResourceManager.dao().getCaptchaRealmAdaptor().selectByCaptchaUser(captchaUser); 28 | 29 | /** 30 | * 用户不存在 31 | */ 32 | if(userAdaptor == null){ 33 | throw new AuthUsernameNotFoundException("用户不存在"); 34 | } 35 | 36 | return getUser(userAdaptor.getUnionid(), userAdaptor.getRoles()); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/util/SecuritySort.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.util; 2 | 3 | /** 4 | * 排序工具 5 | * @author yangyuan 6 | * @date 2018年4月13日 7 | */ 8 | public class SecuritySort implements Comparable{ 9 | /** 10 | * 升序排序 11 | */ 12 | public static final int ORDER_ASC = 1; 13 | 14 | /** 15 | * 降序排序 16 | */ 17 | public static final int ORDER_DESC = -1; 18 | 19 | /** 20 | * 分数 21 | */ 22 | private final int score; 23 | 24 | /** 25 | * 关联对象 26 | */ 27 | private final Object value; 28 | 29 | /** 30 | * 排序方式 31 | *
32 | * 同一组数据,排序方式必须一致 33 | */ 34 | private final int order; 35 | 36 | public SecuritySort(int score, Object value, int order){ 37 | this.score = score; 38 | this.value = value; 39 | this.order = order; 40 | } 41 | 42 | public int getScore() { 43 | return score; 44 | } 45 | 46 | @SuppressWarnings("unchecked") 47 | public T getValue() { 48 | return (T) value; 49 | } 50 | 51 | @Override 52 | public int compareTo(SecuritySort securitySort) { 53 | if(this.getScore() > securitySort.getScore()){ 54 | return this.order; 55 | } 56 | if(this.getScore() < securitySort.getScore()){ 57 | return -this.order; 58 | } 59 | return 0; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/RemoteToken.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | import org.yangyuan.security.core.common.AbstractSecurityToken; 4 | 5 | /** 6 | * 第三方登录令牌 7 | * @author yangyuan 8 | * @date 2017年4月26日 9 | */ 10 | public abstract class RemoteToken extends AbstractSecurityToken { 11 | /** 12 | * 第三方令牌 13 | */ 14 | private final String accessToken; 15 | 16 | /** 17 | * 构造方法 18 | * @param accessToken 第三方令牌 19 | * @param remember 记住我 20 | */ 21 | public RemoteToken(String accessToken, boolean remember) { 22 | super(remember); 23 | this.accessToken = accessToken; 24 | } 25 | 26 | /** 27 | * 第三方平台代码 28 | *
29 | * 1 qq, 2 微信, 3 微博 30 | * 31 | * @return 32 | */ 33 | public abstract int getPlanform(); 34 | 35 | public String getAccessToken() { 36 | return accessToken; 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | StringBuilder builder = new StringBuilder(128); 42 | 43 | builder.append(super.toString()); 44 | 45 | builder.append("[accessToken]("); 46 | builder.append(getAccessToken()); 47 | builder.append(")\n"); 48 | 49 | builder.append("[planform]("); 50 | builder.append(getPlanform()); 51 | builder.append(")\n"); 52 | 53 | return new String(builder); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/WxRemoteToken.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | /** 4 | * 微信第三方登录令牌 5 | * @author yangyuan 6 | * @date 2018年4月3日 7 | */ 8 | public class WxRemoteToken extends RemoteToken{ 9 | /** 10 | * 授权用户唯一标识 11 | */ 12 | private final String openid; 13 | 14 | /** 15 | * remember固定为true的构造方法 16 | * @param accessToken 第三方令牌 17 | * @param openid 授权用户唯一标识 18 | */ 19 | public WxRemoteToken(String accessToken, String openid) { 20 | super(accessToken, true); 21 | this.openid = openid; 22 | } 23 | 24 | /** 25 | * 自定义remember的构造方法 26 | * @param accessToken 第三方令牌 27 | * @param openid 授权用户唯一标识 28 | * @param remember 记住我 29 | */ 30 | public WxRemoteToken(String accessToken, String openid, boolean remember) { 31 | super(accessToken, remember); 32 | this.openid = openid; 33 | } 34 | 35 | public String getOpenid() { 36 | return openid; 37 | } 38 | 39 | @Override 40 | public int getPlanform() { 41 | return 2; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | StringBuilder builder = new StringBuilder(128); 47 | 48 | builder.append(super.toString()); 49 | 50 | builder.append("[openid]("); 51 | builder.append(getOpenid()); 52 | builder.append(")\n"); 53 | 54 | return new String(builder); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/common/SecurityAuthHandler.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core.common; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.servlet.http.HttpServletResponse; 5 | 6 | import org.springframework.web.method.HandlerMethod; 7 | 8 | /** 9 | * 认证回调定义 10 | * @author yangyuan 11 | * @date 2018年4月2日 12 | */ 13 | public interface SecurityAuthHandler { 14 | 15 | /** 16 | * 认证成功 17 | * @param request current HTTP request 18 | * @param response current HTTP response 19 | * @param handler chosen handler to execute, for type and/or instance evaluation 20 | */ 21 | void onSuccess(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler); 22 | 23 | /** 24 | * 基础认证失败(未登录) 25 | * @param request request current HTTP request 26 | * @param response response current HTTP response 27 | * @param handler handler chosen handler to execute, for type and/or instance evaluation 28 | */ 29 | void onAuthFail(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler); 30 | 31 | /** 32 | * 角色认证失败(无权限) 33 | * @param request request current HTTP request 34 | * @param response response current HTTP response 35 | * @param handler handler chosen handler to execute, for type and/or instance evaluation 36 | */ 37 | void onForbiddenFail(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler); 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/filter/AuthcSecurityFilter.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.filter; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | 5 | import org.yangyuan.security.bean.Permission; 6 | import org.yangyuan.security.core.DefaultSubject; 7 | import org.yangyuan.security.core.annotation.SecurityFilterComponent; 8 | import org.yangyuan.security.exception.SecurityFilterAuthException; 9 | import org.yangyuan.security.exception.common.FilterException; 10 | import org.yangyuan.security.filter.common.SecurityFilter; 11 | import org.yangyuan.security.util.SecurityUtils; 12 | 13 | /** 14 | * 基础认证实现 15 | * @author yangyuan 16 | * @date 2017年4月26日 17 | */ 18 | @SecurityFilterComponent(value = "index/2") 19 | public class AuthcSecurityFilter implements SecurityFilter{ 20 | 21 | private static final String FILETER_NAME = "authc"; 22 | 23 | @Override 24 | public boolean approve(Permission permission) { 25 | return permission.getName().toLowerCase().equals(FILETER_NAME); 26 | } 27 | 28 | @Override 29 | public void doFilter(Permission permission, HttpServletRequest request) throws FilterException{ 30 | DefaultSubject subject = SecurityUtils.getSubject(); 31 | 32 | /** 33 | * 未登录 34 | */ 35 | if(subject == null){ 36 | throw new SecurityFilterAuthException(); 37 | } 38 | 39 | /** 40 | * 登录无效 41 | */ 42 | if(!subject.isValid()){ 43 | throw new SecurityFilterAuthException(); 44 | } 45 | 46 | return; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/realm/bean/RemoteUser.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.realm.bean; 2 | 3 | /** 4 | * 第三方授权用户数据 5 | * @author yangyuan 6 | * @date 2018年5月31日 7 | */ 8 | public class RemoteUser { 9 | 10 | /** 11 | * 构造方法 12 | * @param nickname 用户昵称 13 | * @param portrait 用户头像地址 14 | * @param openid 第三方授权唯一id 15 | */ 16 | public RemoteUser(String nickname, String portrait, String openid){ 17 | this.nickname = nickname; 18 | this.portrait = portrait; 19 | this.openid = openid; 20 | } 21 | 22 | /** 23 | * 用户昵称 24 | */ 25 | private final String nickname; 26 | 27 | /** 28 | * 用户头像地址 29 | */ 30 | private final String portrait; 31 | 32 | /** 33 | * 第三方授权唯一id 34 | */ 35 | private final String openid; 36 | 37 | public String getNickname() { 38 | return nickname; 39 | } 40 | 41 | public String getPortrait() { 42 | return portrait; 43 | } 44 | 45 | public String getOpenid() { 46 | return openid; 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | StringBuilder builder = new StringBuilder(128); 52 | 53 | builder.append("[nickname]("); 54 | builder.append(getNickname()); 55 | builder.append(")\n"); 56 | 57 | builder.append("[portrait]("); 58 | builder.append(getPortrait()); 59 | builder.append(")\n"); 60 | 61 | builder.append("[openid]("); 62 | builder.append(getOpenid()); 63 | builder.append(")\n"); 64 | 65 | return new String(builder); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/common/SecurityManager.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core.common; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.servlet.http.HttpServletResponse; 5 | 6 | import org.yangyuan.security.bean.User; 7 | import org.yangyuan.security.exception.common.AuthenticationException; 8 | 9 | /** 10 | * 安全管理器定义 11 | * @author yangyuan 12 | * @date 2017年4月26日 13 | */ 14 | public interface SecurityManager { 15 | /** 16 | * 登录 17 | *

如果正常执行,说明登录成功

18 | *

登录失败会抛出对应的运行时异常

19 | * @param token 登录令牌 20 | * @param response http响应对象 21 | * @return 实例中必须包含[用户全局唯一id(unionid)]和[用户角色列表(roles)] 22 | * @throws AuthenticationException 安全认证相关异常,具体含义参考子类定义 23 | */ 24 | User login(SecurityToken token, HttpServletResponse response) throws AuthenticationException; 25 | 26 | /** 27 | * 登出(当前上下文) 28 | * @param response http响应对象 29 | */ 30 | void logout(HttpServletResponse response); 31 | 32 | /** 33 | * 登出(指定主题) 34 | * @param response http响应对象 35 | * @param subject 主题 36 | */ 37 | void logout(HttpServletResponse response, Subject subject); 38 | 39 | /** 40 | * 统一认证 41 | *

此方法是实现@Security注解的入口

42 | * @param permission 认证表达式 43 | * @param request http请求对象 44 | * @param response http响应对象 45 | * @param handler handler chosen handler to execute, for type and/or instance evaluation 46 | * @return 47 | * true 认证成功 48 | *
49 | * false 认证失败 50 | */ 51 | boolean auth(String permission, HttpServletRequest request, HttpServletResponse response, Object handler); 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/util/SecurityMD5.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.util; 2 | 3 | import java.security.MessageDigest; 4 | 5 | import org.yangyuan.security.config.ResourceManager; 6 | 7 | /** 8 | * MD5摘要工具 9 | * @author yangyuan 10 | * @date 2017年5月4日 11 | */ 12 | public class SecurityMD5 { 13 | /** 14 | * 计算MD5 15 | * @param text 文本 16 | * @return MD5摘要十六进制小写字符串表示 17 | */ 18 | public static String encode(String text) { 19 | try { 20 | MessageDigest md = MessageDigest.getInstance("MD5"); 21 | String md5 = SecurityHexUtil.byteArrayToHexString(md.digest(text.getBytes(ResourceManager.core().getCharset()))); 22 | 23 | return md5; 24 | } catch (Exception e) { 25 | throw new SecurityException(e); 26 | } 27 | } 28 | 29 | /** 30 | * 计算MD5 31 | * @param bytes 字节数组 32 | * @return MD5摘要十六进制小写字符串表示 33 | */ 34 | public static String encode(byte[] bytes) { 35 | try { 36 | MessageDigest md = MessageDigest.getInstance("MD5"); 37 | String md5 = SecurityHexUtil.byteArrayToHexString(md.digest(bytes)); 38 | 39 | return md5; 40 | } catch (Exception e) { 41 | throw new SecurityException(e); 42 | } 43 | } 44 | 45 | /** 46 | * 计算MD5 47 | * @param bytes 字节数组 48 | * @return MD5摘要原始字节数组 49 | */ 50 | public static byte[] simpleEncode(byte[] bytes) { 51 | try { 52 | MessageDigest md = MessageDigest.getInstance("MD5"); 53 | 54 | return md.digest(bytes); 55 | } catch (Exception e) { 56 | throw new SecurityException(e); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/http/response/SimpleResponse.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.http.response; 2 | 3 | import java.nio.charset.Charset; 4 | 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.apache.http.Header; 7 | 8 | /** 9 | * 通用HTTP响应封装 10 | * @author 杨元 11 | * @date 2018年4月17日 12 | */ 13 | public class SimpleResponse { 14 | 15 | /** 16 | * 响应状态码 17 | */ 18 | private int code; 19 | 20 | /** 21 | * 响应体编码 22 | */ 23 | private String charset; 24 | 25 | /** 26 | * 响应内容 27 | */ 28 | private byte[] body; 29 | 30 | /** 31 | * 响应头信息 32 | */ 33 | private Header[] headers; 34 | 35 | public int getCode() { 36 | return code; 37 | } 38 | 39 | public void setCode(int code) { 40 | this.code = code; 41 | } 42 | 43 | /** 44 | * 获取响应体原始字节数组 45 | * @return 响应体字节数组 46 | */ 47 | public byte[] getByteBody() { 48 | return body; 49 | } 50 | 51 | /** 52 | * 获取响应体编码后的字符串 53 | * @return 响应体字符串 54 | */ 55 | public String getStringBody() { 56 | if(body == null){ 57 | return StringUtils.EMPTY; 58 | } 59 | 60 | return new String(body, Charset.forName(charset)); 61 | } 62 | 63 | public void setBody(byte[] body) { 64 | this.body = body; 65 | } 66 | 67 | public Header[] getHeaders() { 68 | return headers; 69 | } 70 | 71 | public void setHeaders(Header[] headers) { 72 | this.headers = headers; 73 | } 74 | 75 | /** 76 | * 设置响应体编码 77 | * @param charset 编码名称 78 | */ 79 | public void setCharset(String charset) { 80 | this.charset = charset; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/config/proxy/SecurityAuthHandlerProxy.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.config.proxy; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.servlet.http.HttpServletResponse; 5 | 6 | import org.springframework.web.method.HandlerMethod; 7 | import org.yangyuan.security.core.common.SecurityAuthHandler; 8 | 9 | /** 10 | * 认证回调代理 11 | * @author yangyuan 12 | * @date 2018年6月20日 13 | */ 14 | public class SecurityAuthHandlerProxy implements SecurityAuthHandler{ 15 | 16 | private final SecurityAuthHandler securityAuthHandler; 17 | 18 | public SecurityAuthHandlerProxy(SecurityAuthHandler securityAuthHandler){ 19 | this.securityAuthHandler = securityAuthHandler; 20 | } 21 | 22 | private void assertSecurityAuthHandlerNotNull(){ 23 | if(securityAuthHandler == null){ 24 | throw new SecurityException("Load security.properties[core.securityAuthHandler] failed!"); 25 | } 26 | } 27 | 28 | @Override 29 | public void onSuccess(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler) { 30 | assertSecurityAuthHandlerNotNull(); 31 | securityAuthHandler.onSuccess(request, response, handler); 32 | } 33 | 34 | @Override 35 | public void onAuthFail(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler) { 36 | assertSecurityAuthHandlerNotNull(); 37 | securityAuthHandler.onAuthFail(request, response, handler); 38 | } 39 | 40 | @Override 41 | public void onForbiddenFail(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler) { 42 | assertSecurityAuthHandlerNotNull(); 43 | securityAuthHandler.onForbiddenFail(request, response, handler); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/realm/jdbc/JdbcRealm.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.realm.jdbc; 2 | 3 | import org.yangyuan.security.bean.User; 4 | import org.yangyuan.security.config.ResourceManager; 5 | import org.yangyuan.security.core.UsernamePasswordToken; 6 | import org.yangyuan.security.core.common.PasswordManager; 7 | import org.yangyuan.security.core.common.SecurityToken; 8 | import org.yangyuan.security.exception.AuthPasswordWrongException; 9 | import org.yangyuan.security.exception.AuthUsernameNotFoundException; 10 | import org.yangyuan.security.realm.bean.JdbcUser; 11 | import org.yangyuan.security.realm.bean.JdbcUserAdaptor; 12 | import org.yangyuan.security.realm.common.AbstractRealm; 13 | 14 | /** 15 | * 持久化数据源实现 16 | * @author yangyuan 17 | * @date 2018年3月15日 18 | */ 19 | public class JdbcRealm extends AbstractRealm{ 20 | 21 | @Override 22 | public User getUser(SecurityToken token) { 23 | UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token; 24 | 25 | /** 26 | * 适配数据 27 | */ 28 | JdbcUser jdbcUser = new JdbcUser(usernamePasswordToken.getUsername()); 29 | JdbcUserAdaptor userAdaptor = ResourceManager.dao().getJdbcRealmAdaptor().selectByJdbcUser(jdbcUser); 30 | 31 | /** 32 | * 用户不存在 33 | */ 34 | if(userAdaptor == null){ 35 | throw new AuthUsernameNotFoundException("用户不存在"); 36 | } 37 | 38 | /** 39 | * 密码不正确 40 | */ 41 | if(!PasswordManager.matches(usernamePasswordToken.getPasswrod(), userAdaptor.getPassword())){ 42 | throw new AuthPasswordWrongException("密码不正确"); 43 | } 44 | 45 | return getUser(userAdaptor.getUnionid(), userAdaptor.getRoles()); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/annotation/Security.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * 安全认证注解 10 | *

此注解配置在controller层中,配合spring mvc controller实现安全认证

11 | *

如果controller方法不配置此注解,默认此方法可以被随意访问,无任何限制

12 | *
    13 | *
  • 14 | *

    匿名访问配置(anon):@Security(permission = "anon")

    15 | *

    效果等同于没有配置Security注解

    16 | *
  • 17 | *
  • 18 | *

    基础认证配置(authc):@Security(permission = "authc")

    19 | *

    只要登录,即可自由访问

    20 | *
  • 21 | *
  • 22 | *

    角色认证配置(roles):@Security(permission = "roles[*admin, <vip{5}, vip{9}]")

    23 | *

    角色认证第一步检查是否登录,然后才会验证角色是否匹配

    24 | *

    角色认证支持范围表达,目前支持<(小于)、>(大于)两种操作符,使用范围操作符时,必须在角色名称后指定级别,花括号(name{?})中的数字,即级别

    25 | *

    角色名称中可以使用通配符进行模糊匹配。'?' Matches any single character. '*' Matches any sequence of characters (including the empty sequence).

    26 | *

    roles中的角色可以配置多个,以英文逗号分隔,为并列关系,即逻辑“或”

    27 | *

    本示例表达式代表的含义为:任何[拥有以admin结尾(包括admin)角色]或者[拥有vip角色并且级别为9]或者[拥有vip角色并且级别小于5]的用户,均可通过认证,满足任何一个或多个条件均可

    28 | *
  • 29 | *
  • 30 | *

    HTTP BASIC认证配置(basic):@Security(permission = "basic[username1:password1, username2:password2]")

    31 | *

    HTTP基础认证实现

    32 | *

    以英文冒号分隔用户名和密码

    33 | *

    支持配置多用户,以英文逗号分隔

    34 | *
  • 35 | *
36 | * @author yangyuan 37 | * @date 2017年4月26日 38 | */ 39 | @Target({ElementType.METHOD}) 40 | @Retention(RetentionPolicy.RUNTIME) 41 | public @interface Security { 42 | /** 43 | * 44 | * 认证表达式 45 | * 46 | * @return 47 | */ 48 | String permission() default ""; 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/config/CommonResource.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.config; 2 | 3 | import org.yangyuan.security.dao.common.RedisResourceFactory; 4 | 5 | /** 6 | * 公共资源定义 7 | * @author yangyuan 8 | * @date 2018年4月2日 9 | */ 10 | public class CommonResource { 11 | /** 12 | * redis连接工厂 13 | */ 14 | private final RedisResourceFactory redisResourceFactory; 15 | 16 | public CommonResource(Builder builder){ 17 | this.redisResourceFactory = builder.redisResourceFactory; 18 | } 19 | 20 | public RedisResourceFactory getRedisResourceFactory() { 21 | return redisResourceFactory; 22 | } 23 | 24 | /** 25 | * 公共资源自定义构造器 26 | * @return 27 | */ 28 | public static Builder custom(){ 29 | return new Builder(); 30 | } 31 | 32 | /** 33 | * 公共资源构造器 34 | * @author yangyuan 35 | * @date 2018年4月2日 36 | */ 37 | public static class Builder { 38 | /** 39 | * redis连接工厂 40 | */ 41 | private RedisResourceFactory redisResourceFactory; 42 | 43 | public Builder redisResourceFactory(RedisResourceFactory redisResourceFactory) { 44 | this.redisResourceFactory = redisResourceFactory; 45 | return this; 46 | } 47 | public CommonResource build(){ 48 | return new CommonResource(this); 49 | } 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | StringBuilder builder = new StringBuilder(128); 55 | 56 | builder.append("[RedisResourceFactory]("); 57 | 58 | if(getRedisResourceFactory() == null){ 59 | builder.append("null"); 60 | }else { 61 | builder.append(getRedisResourceFactory().getClass().getName()); 62 | } 63 | 64 | builder.append(")"); 65 | 66 | return new String(builder); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/captcha/common/AbstractPhoneEmailSecurityCaptcha.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.captcha.common; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | import org.yangyuan.security.exception.CaptchaUnknownAccountTypeException; 6 | 7 | /** 8 | * 手机短信、邮件安全认证验证码抽象实现 9 | * @author yangyuan 10 | * @date 2018年5月4日 11 | */ 12 | public abstract class AbstractPhoneEmailSecurityCaptcha extends AbstractSecurityCaptcha{ 13 | /** 14 | * 手机号正则 15 | */ 16 | private static final Pattern PHONE_PATTERN = Pattern.compile("^\\d{11}$"); 17 | /** 18 | * 邮箱正则 19 | */ 20 | private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@]+@([^@]+\\.)+[^@.]+$"); 21 | 22 | /** 23 | * 获取验证码标题文本 24 | * @return 验证码标题文本 25 | */ 26 | protected abstract String title(); 27 | 28 | /** 29 | * 获取验证码内容文本 30 | * @param code 验证码 31 | * @return 验证码内容文本 32 | */ 33 | protected abstract String content(String code); 34 | 35 | /** 36 | * 发送手机验证码 37 | *

此方法是发送手机短信的具体实现

38 | *

一般通过对接阿里云等第三方平台实现短信发送功能

39 | * @param account 手机号 40 | * @param code 验证码 41 | */ 42 | protected abstract void sendToPhone(String account, String code); 43 | 44 | /** 45 | * 发送邮件验证码 46 | *

此方法是发送邮件的具体实现

47 | *

一般通过使用javax.mail包实现企业邮箱操作

48 | * @param account 邮箱 49 | * @param code 验证码 50 | */ 51 | protected abstract void sendToEmail(String account, String code); 52 | 53 | @Override 54 | protected void sendCode(String account, String code) { 55 | if(PHONE_PATTERN.matcher(account).matches()){ 56 | sendToPhone(account, code); 57 | return; 58 | } 59 | if(EMAIL_PATTERN.matcher(account).matches()){ 60 | sendToEmail(account, code); 61 | return; 62 | } 63 | throw new CaptchaUnknownAccountTypeException(); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/http/client/HttpClient.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.http.client; 2 | 3 | import org.apache.http.client.CookieStore; 4 | import org.apache.http.client.config.CookieSpecs; 5 | import org.apache.http.client.config.RequestConfig; 6 | import org.apache.http.impl.client.BasicCookieStore; 7 | import org.apache.http.impl.client.CloseableHttpClient; 8 | import org.apache.http.impl.client.HttpClients; 9 | import org.yangyuan.security.http.client.common.AbstractHttpClient; 10 | 11 | /** 12 | * 13 | * 普通通用HTTP客户端 14 | *

普通的含义即:非HTTP SSL客户端

15 | * @author yangyuan 16 | * @date 2018年4月17日 17 | */ 18 | public class HttpClient extends AbstractHttpClient{ 19 | /** 20 | * 核心请求对象 21 | */ 22 | private CloseableHttpClient httpclient; 23 | 24 | private static final HttpClient CLIENT = new HttpClient(); 25 | 26 | private HttpClient(){ 27 | /** 28 | * 请求配置 29 | */ 30 | RequestConfig globalConfig = RequestConfig. 31 | custom(). 32 | setCookieSpec(CookieSpecs.DEFAULT). 33 | setSocketTimeout(10000). 34 | setConnectTimeout(20000). 35 | setConnectionRequestTimeout(20000). 36 | build(); 37 | /** 38 | * cookie容器 39 | */ 40 | CookieStore cookieStore = new BasicCookieStore(); 41 | 42 | /** 43 | * 核心请求对象 44 | */ 45 | httpclient = HttpClients. 46 | custom(). 47 | setDefaultRequestConfig(globalConfig). 48 | setDefaultCookieStore(cookieStore). 49 | build(); 50 | } 51 | 52 | @Override 53 | protected CloseableHttpClient getCloseableHttpClient() { 54 | return httpclient; 55 | } 56 | 57 | public static HttpClient getClient(){ 58 | return CLIENT; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/bean/BasicAuth.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.bean; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.apache.commons.codec.binary.Base64; 7 | import org.yangyuan.security.config.ResourceManager; 8 | 9 | /** 10 | * http basic authentication认证模型 11 | * @author yangyuan 12 | * @date 2018年4月12日 13 | */ 14 | public class BasicAuth { 15 | /** 16 | * 表达式分隔符 17 | */ 18 | public static final String SEPARATOR = ","; 19 | 20 | /** 21 | * 授权列表 22 | */ 23 | private final List authorizations; 24 | 25 | private BasicAuth(List authorizations){ 26 | this.authorizations = authorizations; 27 | } 28 | 29 | /** 30 | * 认证授权 31 | * @param authorization 授权 32 | * @return 33 | * true 认证成功 34 | *
35 | * false 认证失败 36 | */ 37 | public boolean contains(String authorization){ 38 | return authorizations.contains(authorization); 39 | } 40 | 41 | /** 42 | * 解析http basic authentication表达式 43 | * @param users http basic authentication表达式 44 | * @return http basic authentication认证模型实例 45 | */ 46 | public static BasicAuth parseBasicAuth(String users){ 47 | try { 48 | /** 49 | * 解析授权 50 | */ 51 | String[] _users = users.split(SEPARATOR); 52 | List authorizations = new ArrayList(); 53 | String authorization; 54 | byte[] bytes; 55 | for(String user : _users){ 56 | bytes = user.getBytes(ResourceManager.core().getCharset()); 57 | bytes = Base64.encodeBase64(bytes); 58 | authorization = new String(bytes, ResourceManager.core().getCharset()); 59 | authorizations.add(authorization); 60 | } 61 | 62 | return new BasicAuth(authorizations); 63 | } catch (Exception e) { 64 | throw new SecurityException(e); 65 | } 66 | } 67 | 68 | @Override 69 | public String toString() { 70 | return this.authorizations.toString(); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/http/client/common/AbstractSSLHttpClient.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.http.client.common; 2 | 3 | import org.apache.http.client.CookieStore; 4 | import org.apache.http.client.config.CookieSpecs; 5 | import org.apache.http.client.config.RequestConfig; 6 | import org.apache.http.conn.ssl.SSLConnectionSocketFactory; 7 | import org.apache.http.impl.client.BasicCookieStore; 8 | import org.apache.http.impl.client.CloseableHttpClient; 9 | import org.apache.http.impl.client.HttpClients; 10 | 11 | /** 12 | * 13 | * 抽象HTTP SSL客户端骨架 14 | *

所有HTTP SSL客户端实现都是专用的,不能通用

15 | * @author yangyuan 16 | * @date 2018年4月17日 17 | */ 18 | public abstract class AbstractSSLHttpClient extends AbstractHttpClient{ 19 | /** 20 | * 核心请求对象 21 | */ 22 | private CloseableHttpClient httpclient; 23 | 24 | public AbstractSSLHttpClient(){ 25 | /** 26 | * 请求配置 27 | */ 28 | RequestConfig globalConfig = RequestConfig. 29 | custom(). 30 | setCookieSpec(CookieSpecs.DEFAULT). 31 | setSocketTimeout(10000). 32 | setConnectTimeout(20000). 33 | setConnectionRequestTimeout(20000). 34 | build(); 35 | /** 36 | * cookie容器 37 | */ 38 | CookieStore cookieStore = new BasicCookieStore(); 39 | 40 | /** 41 | * 核心请求对象 42 | */ 43 | httpclient = HttpClients. 44 | custom(). 45 | setDefaultRequestConfig(globalConfig). 46 | setDefaultCookieStore(cookieStore). 47 | setSSLSocketFactory(getSSLSocketFactory()). 48 | build(); 49 | } 50 | 51 | @Override 52 | protected CloseableHttpClient getCloseableHttpClient() { 53 | return httpclient; 54 | } 55 | 56 | /** 57 | * 获取SSL套接字工厂 58 | *

注意:由于此方法在父类构造方法中调用,子类实现该方法时,不能访问子类的成员变量,否则会造成子类数据访问失败!

59 | * @return SSL套接字 60 | */ 61 | protected abstract SSLConnectionSocketFactory getSSLSocketFactory(); 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/spring/SecuritySpringHook.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.spring; 2 | 3 | import java.util.Map; 4 | 5 | import org.apache.commons.logging.Log; 6 | import org.apache.commons.logging.LogFactory; 7 | import org.springframework.beans.BeansException; 8 | import org.springframework.context.ApplicationContext; 9 | import org.springframework.context.ApplicationContextAware; 10 | import org.springframework.context.ApplicationListener; 11 | import org.springframework.context.event.ContextClosedEvent; 12 | import org.springframework.stereotype.Component; 13 | import org.yangyuan.security.core.DefaultCacheManager; 14 | import org.yangyuan.security.dao.RedisSessionDao; 15 | 16 | /** 17 | * spring 钩子 18 | * @author yangyuan 19 | * @date 2018年4月13日 20 | */ 21 | @Component 22 | public class SecuritySpringHook implements ApplicationContextAware, ApplicationListener{ 23 | private static final Log log = LogFactory.getLog(SecuritySpringHook.class); 24 | 25 | private static ApplicationContext applicationContext; 26 | 27 | @Override 28 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 29 | SecuritySpringHook.applicationContext = applicationContext; 30 | } 31 | 32 | /** 33 | * 系统关闭监听 34 | */ 35 | @Override 36 | public void onApplicationEvent(ContextClosedEvent event) { 37 | RedisSessionDao.gcStop(); 38 | DefaultCacheManager.destroy(); 39 | } 40 | 41 | /** 42 | * 根据bean类型获取spring ioc管理的bean实例 43 | * @param type bean类型,class或interface 44 | * @return bean实例集合 45 | */ 46 | public static Map getBeansOfType(Class type){ 47 | return applicationContext.getBeansOfType(type); 48 | } 49 | 50 | /** 51 | * 根据bean名称获取spring ioc管理的bean实例 52 | * @param name bean名称 53 | * @return bean实例 54 | */ 55 | @SuppressWarnings("unchecked") 56 | public static T getBean(String name) { 57 | T t = null; 58 | try { 59 | t = (T) applicationContext.getBean(name); 60 | } catch (Exception e) { 61 | log.warn("can't load bean[" + name + "] from spring ioc container!", e); 62 | } 63 | return t; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/filter/RoleSecurityFilter.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.filter; 2 | 3 | import java.util.List; 4 | 5 | import javax.servlet.http.HttpServletRequest; 6 | 7 | import org.yangyuan.security.bean.Permission; 8 | import org.yangyuan.security.bean.Role; 9 | import org.yangyuan.security.core.DefaultSubject; 10 | import org.yangyuan.security.core.annotation.SecurityFilterComponent; 11 | import org.yangyuan.security.exception.SecurityFilterForbiddenException; 12 | import org.yangyuan.security.exception.common.FilterException; 13 | import org.yangyuan.security.filter.common.AbstractSecurityCacheFilter; 14 | import org.yangyuan.security.util.SecurityUtils; 15 | 16 | /** 17 | * 角色认证实现 18 | * @author yangyuan 19 | * @date 2017年4月26日 20 | */ 21 | @SecurityFilterComponent(value = "index/3") 22 | public class RoleSecurityFilter extends AbstractSecurityCacheFilter{ 23 | 24 | private static final String FILETER_NAME = "roles"; 25 | 26 | @Override 27 | public boolean approve(Permission permission) { 28 | return permission.getName().toLowerCase().startsWith(FILETER_NAME); 29 | } 30 | 31 | @Override 32 | public void doFilter(Permission permission, HttpServletRequest request) throws FilterException{ 33 | DefaultSubject subject = SecurityUtils.getSubject();; 34 | 35 | /** 36 | * 未登录 37 | */ 38 | if(subject == null){ 39 | throw new SecurityFilterForbiddenException(); 40 | } 41 | 42 | /** 43 | * 登录无效 44 | */ 45 | if(!subject.isValid()){ 46 | throw new SecurityFilterForbiddenException(); 47 | } 48 | 49 | /** 50 | * 解析 51 | */ 52 | List roles = cached(permission.getPermission()); //优先从缓存中获取 53 | if(roles == null){ //缓存未命中 54 | roles = Role.parseRoles(permission.getValue()); //解析 55 | caching(permission.getPermission(), roles); //缓存 56 | } 57 | 58 | /** 59 | * 角色认证 60 | */ 61 | if(SecurityUtils.hasAnyRole(roles)){ 62 | return; //认证通过 63 | } 64 | 65 | /** 66 | * 角色认证未通过 67 | */ 68 | throw new SecurityFilterForbiddenException(); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/servlet/SecurityInterceptor.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.servlet; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.servlet.http.HttpServletResponse; 5 | 6 | import org.springframework.web.method.HandlerMethod; 7 | import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 8 | import org.yangyuan.security.config.ResourceManager; 9 | import org.yangyuan.security.core.DefaultSubject; 10 | import org.yangyuan.security.core.SessionManager; 11 | import org.yangyuan.security.core.annotation.Security; 12 | 13 | /** 14 | * spring mvc 拦截器,实现认证拦截 15 | * @author yangyuan 16 | * @date 2017年4月26日 17 | */ 18 | public class SecurityInterceptor extends HandlerInterceptorAdapter{ 19 | 20 | @Override 21 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 22 | throws Exception { 23 | 24 | /** 25 | * 非controller请求直接放过 26 | */ 27 | if(!handler.getClass().isAssignableFrom(HandlerMethod.class)){ 28 | return true; 29 | } 30 | 31 | /** 32 | * 用户识别 33 | */ 34 | DefaultSubject subjectClient = DefaultSubject.of(request); 35 | SessionManager.setSubject(subjectClient); //保持客户端principal 36 | if(subjectClient != null){ 37 | DefaultSubject subjectServer = (DefaultSubject) ResourceManager.dao().getEhcacheSessionDao().doRead(subjectClient); 38 | if(subjectServer != null){ 39 | SessionManager.setSubject(subjectServer); 40 | }else{ 41 | subjectServer = (DefaultSubject) ResourceManager.dao().getRedisSessionDao().doRead(subjectClient); 42 | if(subjectServer != null){ 43 | SessionManager.setSubject(subjectServer); 44 | ResourceManager.dao().getEhcacheSessionDao().doCreate(subjectServer); //缓存 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * 获取注解 51 | */ 52 | Security securityAnnotation = ((HandlerMethod) handler).getMethodAnnotation(Security.class); 53 | String permission = null; 54 | if(securityAnnotation != null){ 55 | permission = securityAnnotation.permission(); 56 | } 57 | 58 | /** 59 | * 用户认证 60 | */ 61 | return ResourceManager.core().getSecurityManager().auth(permission, request, response, handler); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | org.yangyuan 6 | security 7 | 0.0.1 8 | jar 9 | 10 | security 11 | http://maven.apache.org 12 | 13 | 14 | UTF-8 15 | 4.3.2.RELEASE 16 | 2.9.0 17 | 4.5.3 18 | 1.2.31 19 | 2.10.3 20 | 1.2 21 | 3.4 22 | 1.9 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.springframework 30 | spring-webmvc 31 | ${spring-version} 32 | 33 | 34 | 35 | 36 | commons-logging 37 | commons-logging 38 | ${commons-logging-version} 39 | 40 | 41 | org.apache.commons 42 | commons-lang3 43 | ${commons-lang3-version} 44 | 45 | 46 | commons-codec 47 | commons-codec 48 | ${commons-codec-version} 49 | 50 | 51 | 52 | 53 | org.apache.httpcomponents 54 | httpclient 55 | ${httpclient-version} 56 | 57 | 58 | org.apache.httpcomponents 59 | httpmime 60 | ${httpclient-version} 61 | 62 | 63 | 64 | 65 | net.sf.ehcache 66 | ehcache 67 | ${ehcache-version} 68 | 69 | 70 | 71 | 72 | redis.clients 73 | jedis 74 | ${redis-version} 75 | 76 | 77 | 78 | 79 | com.alibaba 80 | fastjson 81 | ${fastjson-version} 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/common/AbstractCookie.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core.common; 2 | 3 | import org.yangyuan.security.config.ResourceManager; 4 | 5 | /** 6 | * security cookie抽象实现 7 | * @author yangyuan 8 | * @date 2018年3月5日 9 | */ 10 | public abstract class AbstractCookie implements Cookie{ 11 | 12 | /** 13 | * 获取cookie生命周期 14 | * @return an integer specifying the maximum age of the cookie in seconds; if negative, means the cookie is not stored; if zero, deletes the cookie 15 | */ 16 | protected int getMaxAge(){ 17 | return ResourceManager.cookie().getMaxAge(); 18 | } 19 | 20 | protected abstract String getName(); 21 | protected abstract String getValue(); 22 | 23 | protected String getDomain(){ 24 | return ResourceManager.cookie().getDomain(); 25 | } 26 | 27 | protected String getPath(){ 28 | return ResourceManager.cookie().getPath(); 29 | } 30 | 31 | protected boolean getHttpOnly(){ 32 | return ResourceManager.cookie().isHttpOnly(); 33 | } 34 | 35 | protected boolean getSecure(){ 36 | return ResourceManager.cookie().isSecure(); 37 | } 38 | 39 | /** 40 | * 构造Http Cookie 41 | * @return 42 | */ 43 | private javax.servlet.http.Cookie buildHttpCookie(){ 44 | javax.servlet.http.Cookie cookie = new javax.servlet.http.Cookie(getName(), getValue()); 45 | cookie.setDomain(getDomain()); 46 | cookie.setPath(getPath()); 47 | cookie.setHttpOnly(getHttpOnly()); 48 | cookie.setSecure(getSecure()); 49 | cookie.setMaxAge(getMaxAge()); 50 | 51 | return cookie; 52 | } 53 | 54 | @Override 55 | public javax.servlet.http.Cookie toHttpCookie() { 56 | return buildHttpCookie(); 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | StringBuilder builder = new StringBuilder(128); 62 | 63 | builder.append("[name]("); 64 | builder.append(getName()); 65 | builder.append(")\n"); 66 | 67 | builder.append("[value]("); 68 | builder.append(getValue()); 69 | builder.append(")\n"); 70 | 71 | builder.append("[domain]("); 72 | builder.append(getDomain()); 73 | builder.append(")\n"); 74 | 75 | builder.append("[path]("); 76 | builder.append(getPath()); 77 | builder.append(")\n"); 78 | 79 | builder.append("[httpOnly]("); 80 | builder.append(getHttpOnly()); 81 | builder.append(")\n"); 82 | 83 | builder.append("[maxAge]("); 84 | builder.append(getMaxAge()); 85 | builder.append(")\n"); 86 | 87 | builder.append("[secure]("); 88 | builder.append(getSecure()); 89 | builder.append(")\n"); 90 | 91 | return new String(builder); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/util/SecurityConfigUtils.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.util; 2 | 3 | import java.io.FileInputStream; 4 | import java.io.InputStream; 5 | import java.util.Properties; 6 | 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.yangyuan.security.config.CoreResource; 9 | 10 | /** 11 | * 配置工具 12 | * @author yangyuan 13 | * @date 2018年3月30日 14 | */ 15 | public class SecurityConfigUtils { 16 | /** 17 | * 配置容器 18 | */ 19 | private static final Properties PROPERTIES = new Properties(); 20 | /** 21 | * 配置文件路径 22 | */ 23 | private static final String PROPERTIES_FILE_PATH = CoreResource.APP_CLASS_PATH + "security.properties"; 24 | 25 | static { 26 | SecurityConfigUtils.init(); 27 | } 28 | 29 | /** 30 | * 初始化配置服务 31 | */ 32 | private static void init(){ 33 | InputStream is = null; 34 | try{ 35 | is = new FileInputStream(PROPERTIES_FILE_PATH); 36 | PROPERTIES.load(is); 37 | } catch(Exception e) { 38 | throw new SecurityException(e); 39 | } finally { 40 | if(is != null){ 41 | try { 42 | is.close(); 43 | } catch (Exception e) {} 44 | is = null; 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * 读取配置 51 | * @param name 配置名称 52 | * @return 配置值 53 | */ 54 | public static String cell(String name){ 55 | String result = PROPERTIES.getProperty(name); 56 | 57 | if(StringUtils.isBlank(result)){ 58 | throw new SecurityException("can't find name[" + name + "] from config[" + PROPERTIES_FILE_PATH + "]!"); 59 | } 60 | 61 | return result; 62 | } 63 | 64 | /** 65 | * 读取配置 66 | * @param name 配置名称 67 | * @return 配置int值 68 | */ 69 | public static int cellInt(String name){ 70 | return Integer.parseInt(cell(name)); 71 | } 72 | 73 | /** 74 | * 读取配置 75 | * @param name 配置名称 76 | * @return 配置long值 77 | */ 78 | public static long cellLong(String name){ 79 | return Long.parseLong(cell(name)); 80 | } 81 | 82 | /** 83 | * 读取配置 84 | * @param name 配置名称 85 | * @return 配置double值 86 | */ 87 | public static double cellDouble(String name){ 88 | return Double.parseDouble(cell(name)); 89 | } 90 | 91 | /** 92 | * 读取配置 93 | * @param name 配置名称 94 | * @return 配置boolean值 95 | */ 96 | public static boolean cellBoolean(String name){ 97 | return Boolean.parseBoolean(cell(name)); 98 | } 99 | 100 | /** 101 | * 读取配置 102 | * @param name 配置名称 103 | * @return 配置string值 104 | */ 105 | public static String cellString(String name){ 106 | return cell(name); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/dao/EhcacheSessionDao.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.dao; 2 | 3 | import java.util.List; 4 | 5 | import org.yangyuan.security.config.ResourceManager; 6 | import org.yangyuan.security.core.common.Subject; 7 | import org.yangyuan.security.dao.common.CacheSessionDao; 8 | 9 | import net.sf.ehcache.Cache; 10 | import net.sf.ehcache.CacheManager; 11 | import net.sf.ehcache.Element; 12 | import net.sf.ehcache.config.CacheConfiguration; 13 | import net.sf.ehcache.config.Configuration; 14 | import net.sf.ehcache.config.DiskStoreConfiguration; 15 | 16 | /** 17 | * ehcache缓存数据访问层实现 18 | * @author yangyuan 19 | * @date 2017年4月26日 20 | */ 21 | public class EhcacheSessionDao implements CacheSessionDao{ 22 | /** 23 | * 缓存名称 24 | */ 25 | private static final String CACHE_NAME = "securitySubjectCache"; 26 | 27 | /** 28 | * 缓存容器 29 | */ 30 | private final Cache cache; 31 | 32 | @SuppressWarnings("deprecation") 33 | public EhcacheSessionDao() { 34 | /** 35 | * 缓存配置 36 | */ 37 | CacheConfiguration cacheConfig = new CacheConfiguration(); 38 | cacheConfig.name(CACHE_NAME) 39 | .maxEntriesLocalHeap(ResourceManager.cache().getMaxElementsInMemory()) 40 | .eternal(ResourceManager.cache().isEternal()) 41 | .timeToIdleSeconds(ResourceManager.cache().getTimeToIdleSeconds()) 42 | .timeToLiveSeconds(ResourceManager.cache().getTimeToLiveSeconds()) 43 | .memoryStoreEvictionPolicy(ResourceManager.cache().getMemoryStoreEvictionPolicy()) 44 | .overflowToDisk(ResourceManager.cache().isOverflowToDisk()) 45 | .diskPersistent(ResourceManager.cache().isDiskPersistent()); 46 | 47 | /** 48 | * 缓存配置容器 49 | */ 50 | Configuration config = new Configuration(); 51 | config.diskStore(new DiskStoreConfiguration().path("java.io.tmpdir")); 52 | config.cache(cacheConfig); 53 | 54 | /** 55 | * 缓存管理器 56 | */ 57 | CacheManager manager = CacheManager.newInstance(config); 58 | cache = manager.getCache(CACHE_NAME); 59 | } 60 | 61 | @SuppressWarnings("unchecked") 62 | @Override 63 | public Subject doRead(Subject subject) { 64 | Element element = cache.get(subject.getPrincipal()); 65 | if(element == null){ 66 | return null; 67 | } 68 | 69 | return (Subject) element.getObjectValue(); 70 | } 71 | 72 | @Override 73 | public void doCreate(Subject subject) { 74 | Element element = new Element(subject.getPrincipal(), subject); 75 | cache.put(element); 76 | } 77 | 78 | @Override 79 | public void doDelete(Subject subject) { 80 | cache.remove(subject.getPrincipal()); 81 | } 82 | 83 | @Override 84 | public List> queryUserSubjects(String userUnionid) { 85 | throw new SecurityException("method not implements"); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/filter/BasicHttpAuthenticationSecurityFilter.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.filter; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.servlet.http.HttpServletResponse; 5 | 6 | import org.apache.commons.lang3.StringUtils; 7 | import org.yangyuan.security.bean.BasicAuth; 8 | import org.yangyuan.security.bean.Permission; 9 | import org.yangyuan.security.core.annotation.SecurityFilterComponent; 10 | import org.yangyuan.security.exception.SecurityFilterBasicAuthException; 11 | import org.yangyuan.security.exception.common.FilterException; 12 | import org.yangyuan.security.filter.common.AbstractSecurityCacheFilter; 13 | 14 | /** 15 | * http basic authentication认证实现 16 | * @author yangyuan 17 | * @date 2018年4月12日 18 | */ 19 | @SecurityFilterComponent(value = "index/4") 20 | public class BasicHttpAuthenticationSecurityFilter extends AbstractSecurityCacheFilter{ 21 | 22 | private static final String FILETER_NAME = "basic"; 23 | 24 | /** 25 | * HTTP basic authentication header 26 | */ 27 | protected static final String AUTHORIZATION_HEADER = "Authorization"; 28 | 29 | /** 30 | * HTTP basic authenticate header 31 | */ 32 | protected static final String AUTHENTICATE_HEADER = "WWW-Authenticate"; 33 | 34 | /** 35 | * HTTP basic authenticate header scheme 36 | */ 37 | protected static final String AUTHENTICATE_SCHEME = HttpServletRequest.BASIC_AUTH; 38 | 39 | @Override 40 | public boolean approve(Permission permission) { 41 | return permission.getName().toLowerCase().startsWith(FILETER_NAME); 42 | } 43 | 44 | @Override 45 | public void doFilter(Permission permission, HttpServletRequest request) throws FilterException{ 46 | /** 47 | * 获取客户端授权凭证 48 | */ 49 | String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER); 50 | if(StringUtils.isBlank(authorizationHeader)){ 51 | throw new SecurityFilterBasicAuthException(); 52 | } 53 | String[] authorizations = authorizationHeader.split(" "); 54 | if(authorizations.length < 2){ 55 | throw new SecurityFilterBasicAuthException(); 56 | } 57 | String authorization = authorizations[1]; 58 | 59 | /** 60 | * 解析 61 | */ 62 | BasicAuth basicAuth = cached(permission.getPermission()); //优先从缓存中获取 63 | if(basicAuth == null){ //缓存未命中 64 | basicAuth = BasicAuth.parseBasicAuth(permission.getValue()); //解析 65 | caching(permission.getPermission(), basicAuth); //缓存 66 | } 67 | 68 | /** 69 | * 认证 70 | */ 71 | if(basicAuth.contains(authorization)){ 72 | return; //认证通过 73 | } 74 | 75 | /** 76 | * 认证失败 77 | */ 78 | throw new SecurityFilterBasicAuthException(); 79 | } 80 | 81 | /** 82 | * 向客户端响应http basic authentication授权 83 | * @param response http响应对象 84 | */ 85 | public static void sendChallenge(HttpServletResponse response){ 86 | response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 87 | String authenticateHeader = AUTHENTICATE_SCHEME + " realm=\"Security Application\""; 88 | response.setHeader(AUTHENTICATE_HEADER, authenticateHeader); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/bean/User.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.bean; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * 用户模型 7 | * @author yangyuan 8 | * @date 2017年4月26日 9 | */ 10 | public class User { 11 | 12 | /** 13 | * 构造方法 14 | * @param unionid 用户全局唯一id 15 | * @param roles 用户角色列表 16 | */ 17 | public User(String unionid, List roles){ 18 | this.unionid = unionid; 19 | this.roles = roles; 20 | } 21 | 22 | /** 23 | * 用户全局唯一id 24 | */ 25 | private final String unionid; 26 | 27 | /** 28 | * 用户角色列表 29 | */ 30 | private final List roles; 31 | 32 | /** 33 | * 用户昵称 34 | */ 35 | private String nickname; 36 | 37 | /** 38 | * 用户头像地址 39 | */ 40 | private String portrait; 41 | 42 | public String getUnionid() { 43 | return unionid; 44 | } 45 | 46 | public List getRoles() { 47 | return roles; 48 | } 49 | 50 | public String getNickname() { 51 | return nickname; 52 | } 53 | 54 | public void setNickname(String nickname) { 55 | this.nickname = nickname; 56 | } 57 | 58 | public String getPortrait() { 59 | return portrait; 60 | } 61 | 62 | public void setPortrait(String portrait) { 63 | this.portrait = portrait; 64 | } 65 | 66 | @Override 67 | public String toString() { 68 | StringBuilder builder = new StringBuilder(256); 69 | 70 | builder.append("{"); 71 | 72 | builder.append("\"unionid\": "); 73 | if(this.unionid == null){ 74 | builder.append("null, "); 75 | }else{ 76 | builder.append("\""); 77 | builder.append(this.unionid); 78 | builder.append("\", "); 79 | } 80 | 81 | builder.append("\"nickname\": "); 82 | if(this.nickname == null){ 83 | builder.append("null, "); 84 | }else{ 85 | builder.append("\""); 86 | builder.append(this.nickname); 87 | builder.append("\", "); 88 | } 89 | 90 | builder.append("\"portrait\": "); 91 | if(this.portrait == null){ 92 | builder.append("null, "); 93 | }else{ 94 | builder.append("\""); 95 | builder.append(this.portrait); 96 | builder.append("\", "); 97 | } 98 | 99 | builder.append("\"roles\": "); 100 | if(this.roles == null){ 101 | builder.append("null"); 102 | }else if(this.roles.size() == 0){ 103 | builder.append("[]"); 104 | }else { 105 | builder.append("["); 106 | int limit = this.roles.size() - 1; 107 | for(int i = 0; i < this.roles.size(); i++){ 108 | builder.append("\""); 109 | builder.append(this.roles.get(i).toString()); 110 | builder.append("\""); 111 | 112 | if(i != limit){ 113 | builder.append(", "); 114 | } 115 | } 116 | builder.append("]"); 117 | } 118 | 119 | builder.append("}"); 120 | 121 | return new String(builder); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/bean/Permission.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.bean; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | /** 6 | * 认证表达式模型 7 | * @author yangyuan 8 | * @date 2018年6月13日 9 | */ 10 | public class Permission { 11 | /** 12 | * 表达式值开始标记 13 | */ 14 | private static final char PERMISSION_VALUE_PREFIX = '['; 15 | 16 | /** 17 | * 表达式值结束标记 18 | */ 19 | private static final char PERMISSION_VALUE_SUFFIX = ']'; 20 | 21 | /** 22 | * 表达式名称 23 | */ 24 | private final String name; 25 | 26 | /** 27 | * 表达式值 28 | */ 29 | private final String value; 30 | 31 | public Permission(String name, String value){ 32 | this.name = name; 33 | this.value = value; 34 | } 35 | 36 | /** 37 | * 获取表达式名称 38 | * @return 表达式名称 39 | */ 40 | public String getName() { 41 | return name; 42 | } 43 | 44 | /** 45 | * 获取表达式值 46 | * @return 表达式值 47 | */ 48 | public String getValue() { 49 | return value; 50 | } 51 | 52 | /** 53 | * 获取认证表达式字符串 54 | * @return 认证表达式字符串 55 | */ 56 | public String getPermission() { 57 | if(StringUtils.isBlank(getValue())){ 58 | return getName(); 59 | } 60 | 61 | return getName() + PERMISSION_VALUE_PREFIX + getValue() + PERMISSION_VALUE_SUFFIX; 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return this.getPermission(); 67 | } 68 | 69 | /** 70 | * 解析认证表达式 71 | * @param permission 认证表达式字符串 72 | * @return 认证表达式模型 73 | */ 74 | public static Permission valueOf(String permission){ 75 | /** 76 | * 格式规范 77 | */ 78 | permission = permission.replace(" ", ""); 79 | 80 | int offsetPrefix = -1; 81 | int offsetSuffix = -1; 82 | 83 | /** 84 | * 定位 85 | */ 86 | for(int i = 0; i < permission.length(); i++){ 87 | if(permission.charAt(i) == PERMISSION_VALUE_PREFIX){ 88 | offsetPrefix = i; 89 | continue; 90 | } 91 | if(permission.charAt(i) == PERMISSION_VALUE_SUFFIX){ 92 | offsetSuffix = i; 93 | break; 94 | } 95 | } 96 | 97 | /** 98 | * 有效性预测 99 | */ 100 | if(offsetPrefix * offsetSuffix == 0){ //缺少表达式名称,例如:[xxx] 101 | throw new SecurityException("permission missing name"); 102 | } 103 | if(offsetPrefix * offsetSuffix < 0){ //缺少值开始或结束标记,例如:aa[bb、aabb]、aa]bb[ 104 | throw new SecurityException("permission missing '[' or ']'"); 105 | } 106 | if(offsetSuffix - offsetPrefix == 1){ //缺少表达式值,例如:aa[]。如果不需要表达式值,直接写aa即可。 107 | throw new SecurityException("permission missing value"); 108 | } 109 | 110 | /** 111 | * 取值 112 | */ 113 | String name = permission; 114 | String value = ""; 115 | if(offsetPrefix > 0){ 116 | name = permission.substring(0, offsetPrefix); 117 | value = permission.substring(offsetPrefix + 1, offsetSuffix); 118 | } 119 | 120 | return new Permission(name, value); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/config/SessionResource.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.config; 2 | 3 | /** 4 | * session资源定义 5 | * @author yangyuan 6 | * @date 2018年3月31日 7 | */ 8 | public class SessionResource { 9 | /** 10 | * session有效期,单位毫秒 11 | */ 12 | private final long expiresMilliseconds; 13 | /** 14 | * 是否启用垃圾回收 15 | */ 16 | private final boolean gcOpen; 17 | /** 18 | * 垃圾回收lua脚本 19 | */ 20 | private final String gcScript; 21 | /** 22 | * 垃圾回收执行周期 23 | */ 24 | private final long gcDelaySecond; 25 | 26 | public SessionResource(Builder builder){ 27 | this.expiresMilliseconds = builder.expiresMilliseconds; 28 | this.gcOpen = builder.gcOpen; 29 | this.gcScript = builder.gcScript; 30 | this.gcDelaySecond = builder.gcDelaySecond; 31 | } 32 | 33 | public long getExpiresMilliseconds() { 34 | return expiresMilliseconds; 35 | } 36 | public boolean isGcOpen() { 37 | return gcOpen; 38 | } 39 | public String getGcScript() { 40 | return gcScript; 41 | } 42 | public long getGcDelaySecond() { 43 | return gcDelaySecond; 44 | } 45 | 46 | /** 47 | * 自定义session资源构造器 48 | * @return 49 | */ 50 | public static Builder custom(){ 51 | return new Builder(); 52 | } 53 | 54 | /** 55 | * session资源构造器 56 | * @author yangyuan 57 | * @date 2018年3月31日 58 | */ 59 | public static class Builder { 60 | /** 61 | * session有效期,单位毫秒 62 | */ 63 | private long expiresMilliseconds; 64 | /** 65 | * 是否启用垃圾回收 66 | */ 67 | private boolean gcOpen; 68 | /** 69 | * 垃圾回收lua脚本 70 | */ 71 | private String gcScript; 72 | /** 73 | * 垃圾回收执行周期 74 | */ 75 | private long gcDelaySecond; 76 | 77 | public Builder expiresMilliseconds(long expiresMilliseconds) { 78 | this.expiresMilliseconds = expiresMilliseconds; 79 | return this; 80 | } 81 | public Builder gcOpen(boolean gcOpen) { 82 | this.gcOpen = gcOpen; 83 | return this; 84 | } 85 | public Builder gcScript(String gcScript) { 86 | this.gcScript = gcScript; 87 | return this; 88 | } 89 | public Builder gcDelaySecond(long gcDelaySecond) { 90 | this.gcDelaySecond = gcDelaySecond; 91 | return this; 92 | } 93 | public SessionResource build(){ 94 | return new SessionResource(this); 95 | } 96 | } 97 | 98 | @Override 99 | public String toString() { 100 | StringBuilder builder = new StringBuilder(128); 101 | 102 | builder.append("[expiresMilliseconds]("); 103 | builder.append(getExpiresMilliseconds()); 104 | builder.append(")\n"); 105 | 106 | builder.append("[gcOpen]("); 107 | builder.append(isGcOpen()); 108 | builder.append(")\n"); 109 | 110 | builder.append("[gcScript]("); 111 | builder.append(getGcScript()); 112 | builder.append(")\n"); 113 | 114 | builder.append("[gcDelaySecond]("); 115 | builder.append(getGcDelaySecond()); 116 | builder.append(")\n"); 117 | 118 | return new String(builder); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/config/CaptchaResource.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.config; 2 | 3 | /** 4 | * 验证码资源定义 5 | * @author yangyuan 6 | * @date 2018年5月4日 7 | */ 8 | public class CaptchaResource { 9 | /** 10 | * 普通验证码有效期 11 | */ 12 | private final int normalExpireSecond; 13 | /** 14 | * 普通验证码多次发送最短时间间隔 15 | */ 16 | private final int normalMinIntervalSecond; 17 | /** 18 | * 图形验证码有效期 19 | */ 20 | private final int imageExpireSecond; 21 | /** 22 | * 图形验证码错误统计周期 23 | */ 24 | private final int imageWrongPeriodSecond; 25 | /** 26 | * 图形验证码统计周期内允许最大错误次数 27 | */ 28 | private final int imagePeriodMaxWrongCount; 29 | 30 | private CaptchaResource(Builder builder){ 31 | this.normalExpireSecond = builder.normalExpireSecond; 32 | this.normalMinIntervalSecond = builder.normalMinIntervalSecond; 33 | this.imageExpireSecond = builder.imageExpireSecond; 34 | this.imageWrongPeriodSecond = builder.imageWrongPeriodSecond; 35 | this.imagePeriodMaxWrongCount = builder.imagePeriodMaxWrongCount; 36 | } 37 | 38 | public int getNormalExpireSecond() { 39 | return normalExpireSecond; 40 | } 41 | 42 | public int getNormalMinIntervalSecond() { 43 | return normalMinIntervalSecond; 44 | } 45 | 46 | public int getImageExpireSecond() { 47 | return imageExpireSecond; 48 | } 49 | 50 | public int getImageWrongPeriodSecond() { 51 | return imageWrongPeriodSecond; 52 | } 53 | 54 | public int getImagePeriodMaxWrongCount() { 55 | return imagePeriodMaxWrongCount; 56 | } 57 | 58 | /** 59 | * 自定义验证码资源 60 | * @return 验证码资源构造器 61 | */ 62 | public static Builder custom(){ 63 | return new Builder(); 64 | } 65 | 66 | public static class Builder { 67 | /** 68 | * 普通验证码有效期 69 | */ 70 | private int normalExpireSecond; 71 | /** 72 | * 普通验证码多次发送最短时间间隔 73 | */ 74 | private int normalMinIntervalSecond; 75 | /** 76 | * 图形验证码有效期 77 | */ 78 | private int imageExpireSecond; 79 | /** 80 | * 图形验证码错误统计周期 81 | */ 82 | private int imageWrongPeriodSecond; 83 | /** 84 | * 图形验证码统计周期内允许最大错误次数 85 | */ 86 | private int imagePeriodMaxWrongCount; 87 | 88 | public Builder normalExpireSecond(int normalExpireSecond) { 89 | this.normalExpireSecond = normalExpireSecond; 90 | return this; 91 | } 92 | public Builder normalMinIntervalSecond(int normalMinIntervalSecond) { 93 | this.normalMinIntervalSecond = normalMinIntervalSecond; 94 | return this; 95 | } 96 | public Builder imageExpireSecond(int imageExpireSecond) { 97 | this.imageExpireSecond = imageExpireSecond; 98 | return this; 99 | } 100 | public Builder imageWrongPeriodSecond(int imageWrongPeriodSecond) { 101 | this.imageWrongPeriodSecond = imageWrongPeriodSecond; 102 | return this; 103 | } 104 | public Builder imagePeriodMaxWrongCount(int imagePeriodMaxWrongCount) { 105 | this.imagePeriodMaxWrongCount = imagePeriodMaxWrongCount; 106 | return this; 107 | } 108 | /** 109 | * 构造验证码资源 110 | * @return 验证码资源 111 | */ 112 | public CaptchaResource build(){ 113 | return new CaptchaResource(this); 114 | } 115 | 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/DefaultSession.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Map.Entry; 8 | import java.util.Set; 9 | 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.yangyuan.security.bean.Role; 12 | import org.yangyuan.security.config.ResourceManager; 13 | import org.yangyuan.security.core.common.Session; 14 | 15 | import com.alibaba.fastjson.JSON; 16 | import com.alibaba.fastjson.JSONObject; 17 | 18 | /** 19 | * 默认会话实现 20 | * @author yangyuan 21 | * @date 2017年4月26日 22 | */ 23 | public class DefaultSession implements Session{ 24 | /** 25 | * 会话中保存的用户全局唯一id域 26 | */ 27 | public static final String SESSION_UNIONID = "_unionid"; 28 | 29 | /** 30 | * 会话中保存的用户角色域 31 | */ 32 | public static final String SESSION_ROLES = "_roles"; 33 | 34 | /** 35 | * 会话中保存的记住登陆状态域 36 | */ 37 | public static final String SESSION_REMEMBER = "_remember"; 38 | 39 | /** 40 | * 会话数据容器 41 | */ 42 | private Map container = new HashMap(); 43 | 44 | @Override 45 | public Object get(String key) { 46 | return container.get(key); 47 | } 48 | 49 | @Override 50 | public Object set(String key, Object value) { 51 | return container.put(key, value); 52 | } 53 | 54 | @SuppressWarnings("unchecked") 55 | @Override 56 | public byte[] getBytes() { 57 | try { 58 | Map map = simpleCopy(this.container); 59 | List roles = (List) this.container.get(SESSION_ROLES); 60 | map.put(SESSION_ROLES, ""); 61 | if(roles.size() > 0){ 62 | map.put(SESSION_ROLES, Role.getRoles(roles)); 63 | } 64 | 65 | String json = JSON.toJSONString(map); 66 | return json.getBytes(ResourceManager.core().getCharset()); 67 | } catch (Exception e) { 68 | throw new SecurityException(e); 69 | } 70 | } 71 | 72 | /** 73 | * 反序列化 74 | * @param bytes 原始字节数组 75 | * @return session实例 76 | */ 77 | public static DefaultSession parse(byte[] bytes){ 78 | try { 79 | DefaultSession session = new DefaultSession(); 80 | String json = new String(bytes, ResourceManager.core().getCharset()); 81 | JSONObject jsonObject = JSON.parseObject(json); 82 | Set> entrySet = jsonObject.entrySet(); 83 | for(Entry entry : entrySet){ 84 | session.container.put(entry.getKey(), entry.getValue()); 85 | } 86 | String roles = (String) session.container.get(SESSION_ROLES); 87 | session.container.put(SESSION_ROLES, new ArrayList()); 88 | if(StringUtils.isNotBlank(roles)){ 89 | session.container.put(SESSION_ROLES, Role.parseRoles(roles)); 90 | } 91 | 92 | return session; 93 | } catch (Exception e) { 94 | throw new SecurityException(e); 95 | } 96 | } 97 | 98 | private Map simpleCopy(Map map){ 99 | Map _map = new HashMap(); 100 | Set> entrySet = map.entrySet(); 101 | for(Entry entry : entrySet){ 102 | _map.put(entry.getKey(), entry.getValue()); 103 | } 104 | 105 | return _map; 106 | } 107 | 108 | @Override 109 | public String toString() { 110 | try { 111 | return new String(getBytes(), ResourceManager.core().getCharset()); 112 | } catch (Exception e) { 113 | throw new SecurityException(e); 114 | } 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/SecurityFilterManager.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.Iterator; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Map.Entry; 9 | import java.util.Set; 10 | 11 | import javax.servlet.http.HttpServletRequest; 12 | 13 | import org.apache.commons.lang3.StringUtils; 14 | import org.yangyuan.security.bean.Permission; 15 | import org.yangyuan.security.exception.SecurityFilterErrorException; 16 | import org.yangyuan.security.exception.common.FilterException; 17 | import org.yangyuan.security.filter.common.SecurityFilter; 18 | import org.yangyuan.security.spring.SecuritySpringHook; 19 | import org.yangyuan.security.util.SecuritySort; 20 | 21 | /** 22 | * 安全过滤器管理器 23 | * @author yangyuan 24 | * @date 2017年4月26日 25 | */ 26 | public class SecurityFilterManager { 27 | /** 28 | * 过滤器链 29 | */ 30 | private static final List FILTERS_CHAIN; 31 | 32 | static { 33 | /** 34 | * 加载过滤器 35 | */ 36 | Map filters = SecuritySpringHook.getBeansOfType(SecurityFilter.class); 37 | Set> entrySet = filters.entrySet(); 38 | Iterator> iterator = entrySet.iterator(); 39 | Entry entry; 40 | List sorts = new ArrayList(); 41 | FILTERS_CHAIN = new ArrayList(); 42 | while(iterator.hasNext()){ //获取所有过滤器 43 | entry = iterator.next(); 44 | sorts.add(new SecuritySort(getIndex(entry.getKey()), entry.getValue(), SecuritySort.ORDER_ASC)); 45 | } 46 | Collections.sort(sorts); //排序 47 | SecurityFilter filter; 48 | for(SecuritySort sort : sorts){ //组装过滤器链 49 | filter = sort.getValue(); 50 | FILTERS_CHAIN.add(filter); 51 | } 52 | } 53 | 54 | /** 55 | * 获取过滤器排序序号 56 | * @param name 过滤器名称 57 | * @return 排序序号,如果过滤器名称中未指定顺序,默认为0 58 | */ 59 | private static int getIndex(String name){ 60 | if(name.indexOf("/") < 0){ 61 | return Integer.MAX_VALUE; 62 | } 63 | String[] fragments = name.split("/"); 64 | if(fragments.length < 2){ 65 | return Integer.MAX_VALUE; 66 | } 67 | 68 | return Integer.parseInt(fragments[1]); 69 | } 70 | 71 | /** 72 | * 调用过滤器链 73 | * @param permission 认证表达式 74 | * @param request http请求对象 75 | * @throws FilterException 安全过滤器相关异常,具体含义参考子类定义 76 | */ 77 | public static void doFilter(String permission, HttpServletRequest request) throws FilterException{ 78 | if(StringUtils.isBlank(permission)){ 79 | throw new SecurityFilterErrorException("permission is blank"); 80 | } 81 | 82 | Permission p = Permission.valueOf(permission); 83 | for(SecurityFilter filter : FILTERS_CHAIN){ 84 | if(filter.approve(p)){ 85 | filter.doFilter(p, request); 86 | return; 87 | } 88 | } 89 | 90 | throw new SecurityFilterErrorException("permission express: " + permission + ", none security filter can execute"); 91 | } 92 | 93 | /** 94 | * 过滤器链视图 95 | * @return 96 | */ 97 | public static String view(){ 98 | StringBuilder builder = new StringBuilder(256); 99 | builder.append("[root]"); 100 | for(SecurityFilter filter : FILTERS_CHAIN){ 101 | builder.append("->["); 102 | builder.append(filter.getClass().getName()); 103 | builder.append("]"); 104 | } 105 | 106 | return new String(builder); 107 | } 108 | 109 | @Override 110 | public String toString() { 111 | return view(); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/common/PasswordManager.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core.common; 2 | 3 | import java.nio.charset.Charset; 4 | import java.security.MessageDigest; 5 | import java.security.NoSuchAlgorithmException; 6 | 7 | import org.apache.commons.lang3.ArrayUtils; 8 | import org.apache.commons.lang3.RandomUtils; 9 | import org.yangyuan.security.util.SecurityHexUtil; 10 | import org.yangyuan.security.util.SecurityMD5; 11 | 12 | 13 | /** 14 | * 密码加密管理器 15 | * @author yangyuan 16 | * @date 2017年4月26日 17 | */ 18 | public class PasswordManager { 19 | /** 20 | * hash算法 21 | */ 22 | public static final String DEFAULT_HASH_ALGORITHM = "SHA-256"; 23 | /** 24 | * 编码 25 | */ 26 | public static final String DEFAULT_CHARSET = "ISO-8859-1"; 27 | 28 | /** 29 | * 动态加密 30 | * @param text 明文 31 | * @return 小写形式的66位字符串 32 | */ 33 | public static String encrypt(String text){ 34 | int factor = RandomUtils.nextInt(10, 65); 35 | return encrypt(text, factor); 36 | } 37 | 38 | /** 39 | * 核心加密算法 40 | * @param text 明文 41 | * @param factor 加密因子。范围[10, 64] 42 | * @return 小写形式的66位字符串 43 | */ 44 | private static String encrypt(String text, int factor){ 45 | /** 46 | * md5 47 | * 此步骤建议在自己的真实项目中移除,因为它会弱化密码安全性。作者加上这步是为了项目兼容性升级 48 | */ 49 | text = SecurityMD5.encode(text); 50 | 51 | /** 52 | * hash 53 | */ 54 | String sha256 = sha256(text); 55 | 56 | /** 57 | * reverse 58 | */ 59 | byte[] bytes = sha256.getBytes(Charset.forName(DEFAULT_CHARSET)); 60 | int start = factor % 10; 61 | int end = factor; 62 | ArrayUtils.reverse(bytes, start, end); 63 | 64 | /** 65 | * xor 66 | */ 67 | byte _factor = (byte) factor; 68 | for(int i = 0; i < bytes.length; i++){ 69 | bytes[i] = (byte) (bytes[i] ^ _factor); 70 | } 71 | 72 | /** 73 | * hex 74 | */ 75 | String cert = SecurityHexUtil.byteArrayToHexString(bytes); 76 | 77 | /** 78 | * hash 79 | */ 80 | cert = sha256(cert); 81 | 82 | /** 83 | * factor 84 | */ 85 | cert = cert + factor; 86 | 87 | return cert.toLowerCase(); 88 | } 89 | 90 | /** 91 | * 匹配凭证 92 | * @param text 明文 93 | * @param cert 凭证 94 | * @return 95 | */ 96 | public static boolean matches(String text, String cert){ 97 | int factor = Integer.parseInt(cert.substring(cert.length() - 2, cert.length())); 98 | String _cert = encrypt(text, factor); 99 | 100 | return _cert.equals(cert.toLowerCase()); 101 | } 102 | 103 | /** 104 | * SHA-256实现 105 | * @param text 文本 106 | * @return 摘要 107 | */ 108 | private static String sha256(String text){ 109 | try { 110 | /** 111 | * 创建SHA-256加密对象 112 | */ 113 | MessageDigest messageDigest = MessageDigest.getInstance(DEFAULT_HASH_ALGORITHM); 114 | 115 | /** 116 | * 计算摘要 117 | */ 118 | messageDigest.update(text.getBytes(Charset.forName(DEFAULT_CHARSET))); 119 | byte[] bytes = messageDigest.digest(); 120 | StringBuilder builder = new StringBuilder(128); 121 | for (int i = 0; i < bytes.length; i++){ 122 | String hex = Integer.toHexString(0xff & bytes[i]); 123 | if (hex.length() == 1){ 124 | builder.append('0'); 125 | } 126 | builder.append(hex); 127 | } 128 | 129 | return builder.toString(); 130 | } catch (NoSuchAlgorithmException e) { 131 | throw new SecurityException(e); 132 | } 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/DefaultCacheManager.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | import java.util.concurrent.ExecutorService; 4 | import java.util.concurrent.Executors; 5 | 6 | import org.apache.commons.logging.Log; 7 | import org.apache.commons.logging.LogFactory; 8 | import org.yangyuan.security.config.ResourceManager; 9 | import org.yangyuan.security.core.common.CacheManager; 10 | import org.yangyuan.security.core.common.Subject; 11 | 12 | import redis.clients.jedis.Jedis; 13 | import redis.clients.jedis.JedisPubSub; 14 | 15 | /** 16 | * 缓存管理器默认实现 17 | * @author yangyuan 18 | * @date 2017年6月6日 19 | */ 20 | public class DefaultCacheManager implements CacheManager{ 21 | 22 | private static final Log log = LogFactory.getLog(DefaultCacheManager.class); 23 | 24 | /** 25 | * 消息发布频道 26 | */ 27 | protected static final String MESSAGE_CHANNEL = "service:security:message:channel"; 28 | /** 29 | * redis消息订阅 30 | */ 31 | private static JedisPubSub JEDIS_PUB_SUB; 32 | /** 33 | * 消息消费线程池 34 | */ 35 | private static ExecutorService EXECUTOR_MESSAGE; 36 | /** 37 | * 消息监听线程池 38 | */ 39 | private static ExecutorService EXECUTOR_SUBSCRIBE; 40 | 41 | static { 42 | start(); 43 | } 44 | 45 | /** 46 | * 启动 47 | */ 48 | public static void start(){ 49 | /** 50 | * 创建线程池 51 | */ 52 | EXECUTOR_MESSAGE = Executors.newFixedThreadPool(4); 53 | EXECUTOR_SUBSCRIBE = Executors.newSingleThreadExecutor(); 54 | 55 | /** 56 | * redis消息处理 57 | */ 58 | JEDIS_PUB_SUB = 59 | new JedisPubSub() { 60 | @Override 61 | public void onMessage(String channel, String message) { 62 | final String _message = message; 63 | EXECUTOR_MESSAGE.execute(new Runnable() { 64 | @Override 65 | public void run() { 66 | ResourceManager.dao().getEhcacheSessionDao().doDelete(DefaultSubject.getInstance(_message, false, null)); //缓存失效 67 | } 68 | }); 69 | } 70 | }; 71 | 72 | /** 73 | * redis消息订阅 74 | */ 75 | EXECUTOR_SUBSCRIBE.execute(new Runnable() { 76 | @Override 77 | public void run() { 78 | Jedis redis = null; 79 | try { 80 | redis = ResourceManager.common().getRedisResourceFactory().getResource(); 81 | redis.subscribe(JEDIS_PUB_SUB, MESSAGE_CHANNEL); 82 | }catch (Exception e){ 83 | log.error("[Security][CacheManager][start]", e); 84 | }finally { 85 | if(redis != null){ 86 | redis.close(); 87 | } 88 | } 89 | } 90 | }); 91 | } 92 | 93 | /** 94 | * 销毁 95 | */ 96 | public static void destroy(){ 97 | try { 98 | /** 99 | * 取消消息订阅 100 | */ 101 | JEDIS_PUB_SUB.unsubscribe(MESSAGE_CHANNEL); 102 | 103 | /** 104 | * 关闭线程池 105 | */ 106 | EXECUTOR_SUBSCRIBE.shutdown(); 107 | while(!EXECUTOR_SUBSCRIBE.isTerminated()){} 108 | EXECUTOR_MESSAGE.shutdown(); 109 | while(!EXECUTOR_MESSAGE.isTerminated()){} 110 | } catch (Exception e) { 111 | log.error(e); 112 | } 113 | } 114 | 115 | @Override 116 | public void invalid(Subject subject){ 117 | Jedis redis = null; 118 | try { 119 | redis = ResourceManager.common().getRedisResourceFactory().getResource(); 120 | redis.publish(MESSAGE_CHANNEL, subject.getPrincipal()); 121 | }catch (Exception e){ 122 | log.error("[Security][CacheManager][invalid]", e); 123 | }finally { 124 | if(redis != null){ 125 | redis.close(); 126 | } 127 | } 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/captcha/common/AbstractSecurityCaptcha.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.captcha.common; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.yangyuan.security.config.ResourceManager; 5 | import org.yangyuan.security.exception.CaptchaSendTooFastException; 6 | import org.yangyuan.security.exception.common.CaptchaException; 7 | 8 | import redis.clients.jedis.Jedis; 9 | 10 | /** 11 | * 安全认证验证码抽象实现 12 | * @author yangyuan 13 | * @date 2018年5月3日 14 | */ 15 | public abstract class AbstractSecurityCaptcha implements SecurityCaptcha{ 16 | 17 | /** 18 | * 账号-验证码 redis key 19 | */ 20 | private static final String ACCOUNT_CODE_KEY_TMPL = "service:captcha:{name}:account:{account}:code"; 21 | /** 22 | * 账号-验证码频率限制 redis key 23 | */ 24 | private static final String ACCOUNT_CODE_LIMIT_KEY_TMPL = "service:captcha:{name}:account:{account}:limit"; 25 | 26 | /** 27 | * 发送验证码核心实现 28 | * @param account 账号 29 | * @param code 验证码 30 | */ 31 | protected abstract void sendCode(String account, String code); 32 | 33 | /** 34 | * 获取服务名称 35 | *

用来区分不同的业务,防止不同业务之间数据混乱

36 | *

比如:注册验证码、找回密码验证码等业务,必须维护各自私有的数据,不能共享

37 | * @return 服务名称 38 | */ 39 | protected abstract String name(); 40 | 41 | @Override 42 | public void send(String account) throws CaptchaException{ 43 | Jedis redis = null; 44 | try { 45 | redis = ResourceManager.common().getRedisResourceFactory().getResource(); 46 | 47 | /** 48 | * 生成redis key 49 | */ 50 | String codeKey = tmplRender(ACCOUNT_CODE_KEY_TMPL, account); 51 | String codeLimitKey = tmplRender(ACCOUNT_CODE_LIMIT_KEY_TMPL, account); 52 | 53 | /** 54 | * 发送频率检查 55 | */ 56 | if(redis.exists(codeLimitKey)){ 57 | throw new CaptchaSendTooFastException(); 58 | } 59 | 60 | /** 61 | * 发送验证码 62 | */ 63 | String code = newCode(6); 64 | sendCode(account, code); 65 | 66 | /** 67 | * 记录验证码 68 | */ 69 | redis.set(codeKey, code); 70 | redis.expire(codeKey, ResourceManager.captcha().getNormalExpireSecond()); 71 | 72 | /** 73 | * 发送频率限制 74 | */ 75 | redis.set(codeLimitKey, account); 76 | redis.expire(codeLimitKey, ResourceManager.captcha().getNormalMinIntervalSecond()); 77 | 78 | } finally { 79 | if(redis != null){ 80 | redis.close(); 81 | redis = null; 82 | } 83 | } 84 | } 85 | 86 | @Override 87 | public boolean match(String account, String code) { 88 | Jedis redis = null; 89 | try { 90 | redis = ResourceManager.common().getRedisResourceFactory().getResource(); 91 | 92 | String codeKey = tmplRender(ACCOUNT_CODE_KEY_TMPL, account); 93 | String _code = redis.get(codeKey); 94 | 95 | return StringUtils.isNotBlank(_code) && _code.equalsIgnoreCase(code); 96 | } finally { 97 | if(redis != null){ 98 | redis.close(); 99 | redis = null; 100 | } 101 | } 102 | } 103 | 104 | @Override 105 | public void remove(String account) { 106 | Jedis redis = null; 107 | try { 108 | redis = ResourceManager.common().getRedisResourceFactory().getResource(); 109 | 110 | String codeKey = tmplRender(ACCOUNT_CODE_KEY_TMPL, account); 111 | redis.del(codeKey); 112 | 113 | } finally { 114 | if(redis != null){ 115 | redis.close(); 116 | redis = null; 117 | } 118 | } 119 | } 120 | 121 | /** 122 | * redis key模板渲染 123 | * @param tmpl 模板 124 | * @param account 账号 125 | * @return redis key 126 | */ 127 | private String tmplRender(String tmpl, String account){ 128 | return tmpl.replace("{name}", name()) 129 | .replace("{account}", account); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/config/CookieResource.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.config; 2 | 3 | /** 4 | * cookie资源定义 5 | * @author yangyuan 6 | * @date 2018年3月30日 7 | */ 8 | public class CookieResource { 9 | /** 10 | * cookie名称 11 | */ 12 | private final String name; 13 | /** 14 | * cookie域 15 | */ 16 | private final String domain; 17 | /** 18 | * cookie路径 19 | */ 20 | private final String path; 21 | /** 22 | * cookie可见性 23 | */ 24 | private final boolean httpOnly; 25 | /** 26 | * cookie有效期 27 | */ 28 | private final int maxAge; 29 | /** 30 | * cookie https适配 31 | */ 32 | private final boolean secure; 33 | 34 | public CookieResource(Builder builder){ 35 | this.name = builder.name; 36 | this.domain = builder.domain; 37 | this.path = builder.path; 38 | this.httpOnly = builder.httpOnly; 39 | this.maxAge = builder.maxAge; 40 | this.secure = builder.secure; 41 | } 42 | 43 | public String getName() { 44 | return name; 45 | } 46 | public String getDomain() { 47 | return domain; 48 | } 49 | public String getPath() { 50 | return path; 51 | } 52 | public boolean isHttpOnly() { 53 | return httpOnly; 54 | } 55 | public int getMaxAge() { 56 | return maxAge; 57 | } 58 | public boolean isSecure() { 59 | return secure; 60 | } 61 | 62 | /** 63 | * 自定义cookie资源构造器 64 | * @return 65 | */ 66 | public static Builder custom(){ 67 | return new Builder(); 68 | } 69 | 70 | /** 71 | * cookie资源构造器 72 | * @author yangyuan 73 | * @date 2018年3月30日 74 | */ 75 | public static class Builder { 76 | /** 77 | * cookie名称 78 | */ 79 | private String name; 80 | /** 81 | * cookie域 82 | */ 83 | private String domain; 84 | /** 85 | * cookie路径 86 | */ 87 | private String path; 88 | /** 89 | * cookie可见性 90 | */ 91 | private boolean httpOnly; 92 | /** 93 | * cookie有效期 94 | */ 95 | private int maxAge; 96 | /** 97 | * cookie https适配 98 | */ 99 | private boolean secure; 100 | 101 | public Builder name(String name) { 102 | this.name = name; 103 | return this; 104 | } 105 | public Builder domain(String domain) { 106 | this.domain = domain; 107 | return this; 108 | } 109 | public Builder path(String path) { 110 | this.path = path; 111 | return this; 112 | } 113 | public Builder httpOnly(boolean httpOnly) { 114 | this.httpOnly = httpOnly; 115 | return this; 116 | } 117 | public Builder maxAge(int maxAge) { 118 | this.maxAge = maxAge; 119 | return this; 120 | } 121 | public Builder secure(boolean secure) { 122 | this.secure = secure; 123 | return this; 124 | } 125 | 126 | /** 127 | * 创建cookie资源 128 | * @return 129 | */ 130 | public CookieResource build(){ 131 | return new CookieResource(this); 132 | } 133 | } 134 | 135 | @Override 136 | public String toString() { 137 | StringBuilder builder = new StringBuilder(128); 138 | 139 | builder.append("[name]("); 140 | builder.append(getName()); 141 | builder.append(")\n"); 142 | 143 | builder.append("[domain]("); 144 | builder.append(getDomain()); 145 | builder.append(")\n"); 146 | 147 | builder.append("[path]("); 148 | builder.append(getPath()); 149 | builder.append(")\n"); 150 | 151 | builder.append("[httpOnly]("); 152 | builder.append(isHttpOnly()); 153 | builder.append(")\n"); 154 | 155 | builder.append("[maxAge]("); 156 | builder.append(getMaxAge()); 157 | builder.append(")\n"); 158 | 159 | builder.append("[secure]("); 160 | builder.append(isSecure()); 161 | builder.append(")\n"); 162 | 163 | return new String(builder); 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/config/CacheResource.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.config; 2 | 3 | /** 4 | * 缓存资源定义 5 | * @author yangyuan 6 | * @date 2018年4月18日 7 | */ 8 | public class CacheResource { 9 | /** 10 | * cache在内存中最多可以存放的元素的数量 11 | */ 12 | private final int maxElementsInMemory; 13 | /** 14 | * 缓存是否永驻内存 15 | */ 16 | private final boolean eternal; 17 | /** 18 | * 内存中的元素数量溢出是否写入磁盘 19 | */ 20 | private final boolean overflowToDisk; 21 | /** 22 | * 是否持久化内存中的缓存到磁盘 23 | */ 24 | private final boolean diskPersistent; 25 | /** 26 | * 访问cache中元素的最大间隔时间 27 | */ 28 | private final int timeToIdleSeconds; 29 | /** 30 | * cache中元素的总生存时间,cache中的某个元素从创建到消亡的时间 31 | */ 32 | private final int timeToLiveSeconds; 33 | /** 34 | * 内存存储与释放清理策略 35 | */ 36 | private final String memoryStoreEvictionPolicy; 37 | 38 | private CacheResource(Builder builder){ 39 | this.maxElementsInMemory = builder.maxElementsInMemory; 40 | this.eternal = builder.eternal; 41 | this.overflowToDisk = builder.overflowToDisk; 42 | this.diskPersistent = builder.diskPersistent; 43 | this.timeToIdleSeconds = builder.timeToIdleSeconds; 44 | this.timeToLiveSeconds = builder.timeToLiveSeconds; 45 | this.memoryStoreEvictionPolicy = builder.memoryStoreEvictionPolicy; 46 | } 47 | 48 | public int getMaxElementsInMemory() { 49 | return maxElementsInMemory; 50 | } 51 | public boolean isEternal() { 52 | return eternal; 53 | } 54 | public boolean isOverflowToDisk() { 55 | return overflowToDisk; 56 | } 57 | public boolean isDiskPersistent() { 58 | return diskPersistent; 59 | } 60 | public int getTimeToIdleSeconds() { 61 | return timeToIdleSeconds; 62 | } 63 | public int getTimeToLiveSeconds() { 64 | return timeToLiveSeconds; 65 | } 66 | public String getMemoryStoreEvictionPolicy() { 67 | return memoryStoreEvictionPolicy; 68 | } 69 | 70 | /** 71 | * 自定义缓存资源构造器 72 | * @return 73 | */ 74 | public static Builder custom(){ 75 | return new Builder(); 76 | } 77 | 78 | public static class Builder { 79 | /** 80 | * cache在内存中最多可以存放的元素的数量 81 | */ 82 | private int maxElementsInMemory; 83 | /** 84 | * 缓存是否永驻内存 85 | */ 86 | private boolean eternal; 87 | /** 88 | * 内存中的元素数量溢出是否写入磁盘 89 | */ 90 | private boolean overflowToDisk; 91 | /** 92 | * 是否持久化内存中的缓存到磁盘 93 | */ 94 | private boolean diskPersistent; 95 | /** 96 | * 访问cache中元素的最大间隔时间 97 | */ 98 | private int timeToIdleSeconds; 99 | /** 100 | * cache中元素的总生存时间,cache中的某个元素从创建到消亡的时间 101 | */ 102 | private int timeToLiveSeconds; 103 | /** 104 | * 内存存储与释放清理策略 105 | */ 106 | private String memoryStoreEvictionPolicy; 107 | 108 | public Builder maxElementsInMemory(int maxElementsInMemory) { 109 | this.maxElementsInMemory = maxElementsInMemory; 110 | return this; 111 | } 112 | public Builder eternal(boolean eternal) { 113 | this.eternal = eternal; 114 | return this; 115 | } 116 | public Builder overflowToDisk(boolean overflowToDisk) { 117 | this.overflowToDisk = overflowToDisk; 118 | return this; 119 | } 120 | public Builder diskPersistent(boolean diskPersistent) { 121 | this.diskPersistent = diskPersistent; 122 | return this; 123 | } 124 | public Builder timeToIdleSeconds(int timeToIdleSeconds) { 125 | this.timeToIdleSeconds = timeToIdleSeconds; 126 | return this; 127 | } 128 | public Builder timeToLiveSeconds(int timeToLiveSeconds) { 129 | this.timeToLiveSeconds = timeToLiveSeconds; 130 | return this; 131 | } 132 | public Builder memoryStoreEvictionPolicy(String memoryStoreEvictionPolicy) { 133 | this.memoryStoreEvictionPolicy = memoryStoreEvictionPolicy; 134 | return this; 135 | } 136 | 137 | public CacheResource build(){ 138 | return new CacheResource(this); 139 | } 140 | 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/security.properties: -------------------------------------------------------------------------------- 1 | #Session有效期 2 | #这是一个相对值,相对于用户最后一次访问的时间 3 | #也就是说,只有当用户超过此时间不活跃,Session才会失效 4 | #单位秒(s) 5 | session.expiresMilliseconds=2592000000 6 | #是否启用Session垃圾回收器 7 | session.gc.open=true 8 | #Session垃圾回收器Lua脚本 9 | session.gc.script=for i=48,83,1 do local partition if(i > 57) then partition = string.char(i + 39) else partition = string.char(i) end local setkey = 'security:session:set:'..partition local principals = redis.call('ZRANGEBYSCORE', setkey, '-inf', ARGV[1]) redis.call('ZREMRANGEBYSCORE', setkey, '-inf', ARGV[1]) if(principals and (table.maxn(principals) > 0)) then for ii,vv in ipairs(principals) do local hashkey = 'security:session:hash:'..partition redis.call('HDEL', hashkey, vv) end end end 10 | #Session垃圾回收器执行时间间隔 11 | #单位秒(s) 12 | session.gc.gcDelaySecond=86400 13 | 14 | 15 | 16 | #cookie名称 17 | cookie.name=sid 18 | #cookie域名 19 | cookie.domain=.cospace.xyz 20 | #cookie路径 21 | cookie.path=/ 22 | #此配置为true时,cookie无法通过js脚本操作 23 | cookie.httpOnly=true 24 | #是否启用HTTPS 25 | cookie.secure=true 26 | #cookie有效期,一般不需要改动,目前设置的是最大值,相当于永不过期 27 | #因为cookie的生命周期由服务器端维护,所以客户端不需要关心过期时间 28 | cookie.maxAge=315360000 29 | 30 | 31 | 32 | #Redis客户端连接工厂 33 | #负责提供Redis客户端连接 34 | common.redisResourceFactory=cc.cospace.web.security.dao.DefaultRedisResourceFactory 35 | 36 | 37 | 38 | #安全管理器实现 39 | core.securityManager=org.yangyuan.security.core.DefaultSecurityManager 40 | #安全唯一标识生成器实现 41 | core.principalFactory=org.yangyuan.security.core.DefaultPrincipalFactory 42 | #缓存管理器实现 43 | core.cacheManager=org.yangyuan.security.core.DefaultCacheManager 44 | #是否复用客户端subject 45 | #如果设为true,客户端登陆时如果携带有subject信息,那么复用此subject,不再创建新的subject 46 | #如果设为false,则登录时忽略客户端携带的subject信息,总是创建新的subject 47 | core.useClientSubjectLogin=false 48 | #并发主题控制器 49 | #[org.yangyuan.security.core.MultiportConcurrentSubjectControl]允许同一个账号同时在不同客户端登陆 50 | #[org.yangyuan.security.core.SingleConcurrentSubjectControl]同一个账号同一时刻只能在一个客户端登陆,如果之前在其他客户端登陆过,那么之前的登陆将失效 51 | #[org.yangyuan.security.core.RefuseConcurrentSubjectControl]同一个账号同一时刻只能在一个客户端登陆,如果之前在其他客户端登陆过,那么本次登陆将会失败,除非其他客户端主动退出登陆 52 | core.concurrentSubjectControl=org.yangyuan.security.core.MultiportConcurrentSubjectControl 53 | #认证回调 54 | #此处理器用来响应认证结果(成功、失败、拒绝访问) 55 | #具体的响应依赖于具体的业务,框架只负责通知认证结果 56 | core.securityAuthHandler=cc.cospace.web.security.core.DefaultSecurityAuthHandler 57 | 58 | 59 | 60 | #ehcache缓存数据访问层(缓存层) 61 | dao.ehcacheSessionDao=org.yangyuan.security.dao.EhcacheSessionDao 62 | #redis数据访问层(持久化层) 63 | dao.redisSessionDao=org.yangyuan.security.dao.RedisSessionDao 64 | #持久化数据源(用户名密码模式) 65 | dao.jdbcRealm=org.yangyuan.security.realm.jdbc.JdbcRealm 66 | #持久化数据源(验证码模式) 67 | dao.captchaRealm=org.yangyuan.security.realm.captcha.CaptchaRealm 68 | #第三方数据源 69 | dao.remoteRealm=org.yangyuan.security.realm.remote.RemoteRealm 70 | #本地认证数据访问层(用户名密码模式) 71 | dao.jdbcSessionDao=org.yangyuan.security.dao.JdbcSessionDao 72 | #第三方登录认证数据访问层 73 | dao.remoteSessionDao=org.yangyuan.security.dao.RemoteSessionDao 74 | #用户名密码模式登录适配器 75 | #此适配器实现安全认证与具体项目用户数据存储之间的解耦 76 | dao.jdbcRealmAdaptor=userService 77 | #验证码模式登录适配器 78 | #此适配器实现安全认证与具体项目用户数据存储之间的解耦 79 | dao.captchaRealmAdaptor=userService 80 | #第三方登录适配器 81 | #此适配器实现安全认证与具体项目用户数据存储之间的解耦 82 | dao.remoteRealmAdaptor=userService 83 | 84 | 85 | 86 | #cache在内存中最多可以存放的元素的数量。 87 | #0表示没有限制。 88 | #如果放入cache中的元素超过这个数值,有两种可能: 89 | #1、若overflowToDisk的属性值为true,会将cache中多出的元素放入磁盘文件中。 90 | #2、若overflowToDisk的属性值为false,会根据memoryStoreEvictionPolicy的策略替换cache中原有的元素。 91 | cache.maxElementsInMemory=10000 92 | #缓存是否永驻内存。 93 | #如果值是true,cache中的元素将一直保存在内存中,不会因为时间超时而丢失。 94 | #因此在这个值为true的时候,timeToIdleSeconds和timeToLiveSeconds两个属性的值就不起作用了。 95 | cache.eternal=false 96 | #内存中的元素数量溢出是否写入磁盘。 97 | #系统会根据标签中path的值查找对应的属性值。 98 | #如果系统的java.io.tmpdir的值是/temp,写入磁盘的文件就会放在这个文件夹下,文件的名称是cache的名称,后缀名为data。 99 | cache.overflowToDisk=false 100 | #是否持久化内存中的缓存到磁盘。 101 | #当这个属性的值为true时,系统在初始化的时候会在磁盘中查找文件名为cache名称,后缀名为index的的文件,如CACHE_FUNC.index。 102 | #这个文件中存放了已经持久化在磁盘中的cache的index,找到后把cache加载到内存。 103 | cache.diskPersistent=false 104 | #访问cache中元素的最大间隔时间。 105 | #如果超过此时间cache中的某个元素没有任何访问,那么这个元素将被从cache中清除。 106 | cache.timeToIdleSeconds=900 107 | #cache中元素的总生存时间,cache中的某个元素从创建到消亡的时间。 108 | #从创建开始计时,当超过这个时间,这个元素将被从cache中清除,即便是这个元素被频繁访问。 109 | cache.timeToLiveSeconds=7200 110 | #内存存储与释放清理策略 111 | #LRU最近最少使用 112 | #LFU历史访问频率最低 113 | #FIFO先进先出 114 | cache.memoryStoreEvictionPolicy=LRU 115 | 116 | 117 | 118 | #普通验证码有效期 119 | #单位s 120 | captcha.normal.expireSecond=900 121 | #普通验证码多次发送最短时间间隔 122 | #单位s 123 | captcha.normal.minIntervalSecond=50 124 | #图形验证码有效期 125 | #单位s 126 | captcha.image.expireSecond=600 127 | #图形验证码错误统计周期 128 | #单位s 129 | captcha.image.wrongPeriodSecond=60 130 | #图形验证码统计周期内允许最大错误次数 131 | captcha.image.periodMaxWrongCount=3 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/captcha/common/AbstractSecurityImageCaptcha.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.captcha.common; 2 | 3 | import org.apache.commons.lang3.RandomStringUtils; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.yangyuan.security.config.ResourceManager; 6 | import org.yangyuan.security.exception.common.CaptchaException; 7 | 8 | import redis.clients.jedis.Jedis; 9 | 10 | /** 11 | * 安全认证图形验证码抽象实现 12 | * @author yangyuan 13 | * @date 2018年5月4日 14 | */ 15 | public abstract class AbstractSecurityImageCaptcha implements SecurityImageCaptcha{ 16 | /** 17 | * token-验证码 redis key 18 | */ 19 | private static final String TOKEN_CODE_KEY_TMPL = "service:captcha:image:{name}:token:{token}:code"; 20 | /** 21 | * token-验证码错误频率检测 redis key 22 | */ 23 | private static final String TOKEN_CODE_LIMIT_KEY_TMPL = "service:captcha:image:{name}:token:{token}:limit"; 24 | 25 | /** 26 | * 获取服务名称 27 | *

用来区分不同的业务,防止不同业务之间数据混乱

28 | *

比如:注册图形验证码、登陆图形验证码等业务,必须维护各自私有的数据,不能共享

29 | * @return 服务名称 30 | */ 31 | protected abstract String name(); 32 | 33 | @Override 34 | public void send(String token) throws CaptchaException{ 35 | Jedis redis = null; 36 | try { 37 | redis = ResourceManager.common().getRedisResourceFactory().getResource(); 38 | 39 | /** 40 | * 生成redis key 41 | */ 42 | String codeKey = tmplRender(TOKEN_CODE_KEY_TMPL, token); 43 | String codeLimitKey = tmplRender(TOKEN_CODE_LIMIT_KEY_TMPL, token); 44 | 45 | /** 46 | * 错误频率检查 47 | */ 48 | String code = newCode(4); 49 | String limit = redis.get(codeLimitKey); 50 | if(StringUtils.isNotBlank(limit) 51 | && Integer.parseInt(limit) >= ResourceManager.captcha().getImagePeriodMaxWrongCount()){ 52 | code = newCode(6); 53 | } 54 | 55 | /** 56 | * 记录验证码 57 | */ 58 | redis.set(codeKey, code); 59 | redis.expire(codeKey, ResourceManager.captcha().getImageExpireSecond()); 60 | 61 | } finally { 62 | if(redis != null){ 63 | redis.close(); 64 | redis = null; 65 | } 66 | } 67 | } 68 | 69 | @Override 70 | public boolean match(String token, String code) { 71 | Jedis redis = null; 72 | try { 73 | redis = ResourceManager.common().getRedisResourceFactory().getResource(); 74 | 75 | /** 76 | * 生成redis key 77 | */ 78 | String codeKey = tmplRender(TOKEN_CODE_KEY_TMPL, token); 79 | String codeLimitKey = tmplRender(TOKEN_CODE_LIMIT_KEY_TMPL, token); 80 | 81 | /** 82 | * 匹配验证码 83 | */ 84 | String _code = redis.get(codeKey); 85 | boolean result = StringUtils.isNotBlank(_code) && _code.equalsIgnoreCase(code); 86 | if(result){ //认证成功,清除错误检测 87 | redis.del(codeLimitKey); 88 | }else{ //认证失败,更新错误检测数据 89 | redis.incr(codeLimitKey); 90 | redis.expire(codeLimitKey, ResourceManager.captcha().getImageWrongPeriodSecond()); 91 | } 92 | 93 | return result; 94 | } finally { 95 | if(redis != null){ 96 | redis.close(); 97 | redis = null; 98 | } 99 | } 100 | } 101 | 102 | @Override 103 | public void remove(String token) { 104 | Jedis redis = null; 105 | try { 106 | redis = ResourceManager.common().getRedisResourceFactory().getResource(); 107 | 108 | String codeKey = tmplRender(TOKEN_CODE_KEY_TMPL, token); 109 | redis.del(codeKey); 110 | 111 | } finally { 112 | if(redis != null){ 113 | redis.close(); 114 | redis = null; 115 | } 116 | } 117 | } 118 | 119 | @Override 120 | public String newToken() { 121 | return name() + "_" + RandomStringUtils.randomNumeric(32); 122 | } 123 | 124 | @Override 125 | public String getCode(String token) { 126 | Jedis redis = null; 127 | try { 128 | redis = ResourceManager.common().getRedisResourceFactory().getResource(); 129 | 130 | String codeKey = tmplRender(TOKEN_CODE_KEY_TMPL, token); 131 | 132 | return redis.get(codeKey); 133 | } finally { 134 | if(redis != null){ 135 | redis.close(); 136 | redis = null; 137 | } 138 | } 139 | } 140 | 141 | /** 142 | * redis key模板渲染 143 | * @param tmpl 模板 144 | * @param token 令牌 145 | * @return redis key 146 | */ 147 | private String tmplRender(String tmpl, String token){ 148 | return tmpl.replace("{name}", name()) 149 | .replace("{token}", token); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/DefaultSecurityManager.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | import javax.servlet.http.Cookie; 4 | import javax.servlet.http.HttpServletRequest; 5 | import javax.servlet.http.HttpServletResponse; 6 | 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.springframework.web.method.HandlerMethod; 9 | import org.yangyuan.security.bean.User; 10 | import org.yangyuan.security.config.ResourceManager; 11 | import org.yangyuan.security.core.common.CacheManager; 12 | import org.yangyuan.security.core.common.SecurityManager; 13 | import org.yangyuan.security.core.common.SecurityToken; 14 | import org.yangyuan.security.core.common.Subject; 15 | import org.yangyuan.security.exception.SecurityFilterAuthException; 16 | import org.yangyuan.security.exception.SecurityFilterBasicAuthException; 17 | import org.yangyuan.security.exception.SecurityFilterForbiddenException; 18 | import org.yangyuan.security.exception.common.AuthenticationException; 19 | import org.yangyuan.security.filter.BasicHttpAuthenticationSecurityFilter; 20 | import org.yangyuan.security.util.SecurityUtils; 21 | 22 | /** 23 | * 默认安全管理器实现 24 | * @author yangyuan 25 | * @date 2017年4月26日 26 | */ 27 | public class DefaultSecurityManager implements SecurityManager{ 28 | 29 | @Override 30 | public User login(SecurityToken token, HttpServletResponse response) throws AuthenticationException { 31 | 32 | /** 33 | * 识别登录方式 34 | */ 35 | User user = null; 36 | if(token instanceof JdbcToken){ //本地登录 37 | user = ResourceManager.dao().getJdbcSessionDao().auth(token); 38 | } 39 | if(token instanceof RemoteToken){ //第三方登录 40 | user = ResourceManager.dao().getRemoteSessionDao().auth(token); 41 | } 42 | 43 | /** 44 | * 并发主题控制 45 | */ 46 | ResourceManager.core().getConcurrentSubjectControl().control(user); 47 | 48 | /** 49 | * 创建subject 50 | */ 51 | DefaultSubject clientSubject = SecurityUtils.getSubject(); 52 | DefaultSubject subject = null; 53 | if(ResourceManager.core().isUseClientSubjectLogin() && clientSubject != null){ 54 | subject = DefaultSubject.valid(clientSubject); 55 | } 56 | if(subject == null){ 57 | subject = DefaultSubject.getInstance(ResourceManager.core().getPrincipalFactory().newPrincipal(user.getUnionid()), true, null); 58 | } 59 | 60 | /** 61 | * subject关联用户 62 | */ 63 | DefaultSession session = (DefaultSession) subject.getSession(); 64 | session.set(DefaultSession.SESSION_UNIONID, user.getUnionid()); 65 | session.set(DefaultSession.SESSION_ROLES, user.getRoles()); 66 | session.set(DefaultSession.SESSION_REMEMBER, token.isRemember()); 67 | ResourceManager.dao().getRedisSessionDao().doCreate(subject); //持久化到redis 68 | ResourceManager.dao().getEhcacheSessionDao().doCreate(subject); //缓存 69 | SessionManager.setSubject(subject); //写入会话 70 | 71 | /** 72 | * 设置客户端响应cookie 73 | */ 74 | Cookie cookie; 75 | if(token.isRemember()){ //记住登陆状态 76 | cookie = new PrincipalPersistentCookie(subject.getPrincipal()).toHttpCookie(); 77 | }else{ //临时登录 78 | cookie = new PrincipalSessionCookie(subject.getPrincipal()).toHttpCookie(); 79 | } 80 | response.addCookie(cookie); 81 | 82 | return user; 83 | } 84 | 85 | @Override 86 | public void logout(HttpServletResponse response) { 87 | DefaultSubject subject = SecurityUtils.getSubject(); 88 | logout(response, subject); 89 | } 90 | 91 | @Override 92 | public void logout(HttpServletResponse response, Subject subject) { 93 | /** 94 | * 未登录直接跳过 95 | */ 96 | if(subject == null){ 97 | return; 98 | } 99 | 100 | /** 101 | * 登录无效直接跳过 102 | */ 103 | if(!subject.isValid()){ 104 | return; 105 | } 106 | 107 | /** 108 | * 服务端数据移除 109 | */ 110 | ResourceManager.dao().getRedisSessionDao().doDelete(subject); 111 | CacheManager cacheManager = ResourceManager.core().getCacheManager(); 112 | cacheManager.invalid(subject); 113 | 114 | /** 115 | * 客户端cookie移除 116 | */ 117 | if(response != null){ 118 | PrincipalInvalidCookie cookie = new PrincipalInvalidCookie(); 119 | response.addCookie(cookie.toHttpCookie()); 120 | } 121 | } 122 | 123 | @Override 124 | public boolean auth(String permission, HttpServletRequest request, HttpServletResponse response, Object handler) { 125 | if(StringUtils.isBlank(permission)){ 126 | return true; 127 | } 128 | 129 | try { 130 | SecurityFilterManager.doFilter(permission, request); 131 | } catch (SecurityFilterAuthException e) { 132 | ResourceManager.core() 133 | .getSecurityAuthHandler() 134 | .onAuthFail(request, response, (HandlerMethod) handler); 135 | return false; 136 | } catch (SecurityFilterForbiddenException e) { 137 | ResourceManager.core() 138 | .getSecurityAuthHandler() 139 | .onForbiddenFail(request, response, (HandlerMethod) handler); 140 | return false; 141 | } catch (SecurityFilterBasicAuthException e) { 142 | BasicHttpAuthenticationSecurityFilter.sendChallenge(response); 143 | return false; 144 | } 145 | 146 | ResourceManager.core() 147 | .getSecurityAuthHandler() 148 | .onSuccess(request, response, (HandlerMethod) handler); 149 | return true; 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/http/client/common/AbstractHttpClient.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.http.client.common; 2 | 3 | import java.util.Map; 4 | import java.util.Map.Entry; 5 | 6 | import org.apache.http.HttpEntity; 7 | import org.apache.http.client.methods.CloseableHttpResponse; 8 | import org.apache.http.client.methods.HttpDelete; 9 | import org.apache.http.client.methods.HttpGet; 10 | import org.apache.http.client.methods.HttpHead; 11 | import org.apache.http.client.methods.HttpPost; 12 | import org.apache.http.client.methods.HttpPut; 13 | import org.apache.http.client.methods.HttpRequestBase; 14 | import org.apache.http.entity.StringEntity; 15 | import org.apache.http.impl.client.CloseableHttpClient; 16 | import org.apache.http.util.EntityUtils; 17 | import org.yangyuan.security.http.response.SimpleResponse; 18 | 19 | /** 20 | * 抽象HTTP客户端骨架 21 | * @author yangyuan 22 | * @date 2018年4月17日 23 | */ 24 | public abstract class AbstractHttpClient { 25 | 26 | /** 27 | * 获取核心请求实例 28 | * @return 29 | */ 30 | protected abstract CloseableHttpClient getCloseableHttpClient(); 31 | 32 | /** 33 | * head请求 34 | * @param url 请求地址 35 | * @return 36 | */ 37 | public SimpleResponse head(String url){ 38 | return head(url, null); 39 | } 40 | 41 | /** 42 | * head请求 43 | * @param url 请求地址 44 | * @param options 请求配置 45 | * @return 46 | */ 47 | public SimpleResponse head(String url, HttpOptions options){ 48 | HttpHead request = new HttpHead(url); 49 | 50 | if(options == null){ 51 | options = new HttpOptions() {}; 52 | } 53 | 54 | return sendRequest(request, options); 55 | } 56 | 57 | /** 58 | * get请求 59 | * @param url 请求地址 60 | * @return 61 | */ 62 | public SimpleResponse get(String url) { 63 | return get(url, null); 64 | } 65 | 66 | /** 67 | * get请求 68 | * @param url 请求地址 69 | * @param options 请求配置 70 | * @return 71 | */ 72 | public SimpleResponse get(String url, HttpOptions options) { 73 | HttpGet request = new HttpGet(url); 74 | 75 | if(options == null){ 76 | options = new HttpOptions() {}; 77 | } 78 | 79 | return sendRequest(request, options); 80 | } 81 | 82 | /** 83 | * post请求 84 | * @param url 请求地址 85 | * @param content 请求体 86 | * @return 87 | */ 88 | public SimpleResponse post(String url, String content) { 89 | return post(url, content, null); 90 | } 91 | 92 | /** 93 | * post请求 94 | * @param url 请求地址 95 | * @param content 请求体 96 | * @param options 请求配置 97 | * @return 98 | */ 99 | public SimpleResponse post(String url, String content, HttpOptions options) { 100 | HttpPost request = new HttpPost(url); 101 | if(options == null){ 102 | options = new HttpOptions() {}; 103 | } 104 | StringEntity entity = null; 105 | try { 106 | entity = new StringEntity(content, options.getRequestBodyCharset()); 107 | } catch (Exception e) { 108 | throw new SecurityException(e); 109 | } 110 | entity.setContentType(options.getContentType()); 111 | request.setEntity(entity); 112 | return sendRequest(request, options); 113 | } 114 | 115 | /** 116 | * put请求 117 | * @param url 请求地址 118 | * @param content 请求体 119 | * @return 120 | */ 121 | public SimpleResponse put(String url, String content) { 122 | return put(url, content, null); 123 | } 124 | 125 | /** 126 | * put请求 127 | * @param url 请求地址 128 | * @param content 请求体 129 | * @param options 请求配置 130 | * @return 131 | */ 132 | public SimpleResponse put(String url, String content, HttpOptions options) { 133 | HttpPut request = new HttpPut(url); 134 | if(options == null){ 135 | options = new HttpOptions() {}; 136 | } 137 | StringEntity entity = null; 138 | try { 139 | entity = new StringEntity(content, options.getRequestBodyCharset()); 140 | } catch (Exception e) { 141 | throw new SecurityException(e); 142 | } 143 | entity.setContentType(options.getContentType()); 144 | request.setEntity(entity); 145 | return sendRequest(request, options); 146 | } 147 | 148 | /** 149 | * delete请求 150 | * @param url 请求地址 151 | * @return 152 | */ 153 | public SimpleResponse delete(String url) { 154 | return delete(url, null); 155 | } 156 | 157 | /** 158 | * delete请求 159 | * @param url 请求地址 160 | * @param options 请求配置 161 | * @return 162 | */ 163 | public SimpleResponse delete(String url, HttpOptions options) { 164 | HttpDelete request = new HttpDelete(url); 165 | if(options == null){ 166 | options = new HttpOptions() {}; 167 | } 168 | return sendRequest(request, options); 169 | } 170 | 171 | /** 172 | * 配置请求头域 173 | * @param request 请求实例 174 | * @param options 配置实例 175 | */ 176 | private void fillHeaders(HttpRequestBase request, HttpOptions options){ 177 | Map headers = options.getHeaders(); 178 | for(Entry entry : headers.entrySet()){ 179 | request.addHeader(entry.getKey(), entry.getValue()); 180 | } 181 | } 182 | 183 | /** 184 | * 核心请求方法 185 | * @param request 请求对象,可以是get、post... 186 | * @return 187 | */ 188 | private SimpleResponse sendRequest(HttpRequestBase request, HttpOptions options) { 189 | //响应对象 190 | CloseableHttpResponse response = null; 191 | //响应封装 192 | SimpleResponse simpleResponse = new SimpleResponse(); 193 | simpleResponse.setCharset(options.getResponseBodyCharset()); 194 | 195 | try { 196 | //填充请求头域 197 | fillHeaders(request, options); 198 | //执行请求 199 | response = getCloseableHttpClient().execute(request); 200 | //获取响应状态码 201 | simpleResponse.setCode(response.getStatusLine().getStatusCode()); 202 | //获取响应header 203 | simpleResponse.setHeaders(response.getAllHeaders()); 204 | //获取响应体 205 | HttpEntity entity = response.getEntity(); 206 | if(entity != null){ 207 | simpleResponse.setBody(EntityUtils.toByteArray(entity)); 208 | } 209 | } catch (Exception e) { 210 | throw new SecurityException(e); 211 | } finally { 212 | try { 213 | if(response != null){ 214 | response.close(); 215 | } 216 | } catch (Exception e) { 217 | response = null; 218 | } 219 | } 220 | 221 | return simpleResponse; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/core/DefaultSubject.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.core; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import javax.servlet.http.Cookie; 8 | import javax.servlet.http.HttpServletRequest; 9 | 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.yangyuan.security.config.ResourceManager; 12 | import org.yangyuan.security.core.common.Session; 13 | import org.yangyuan.security.core.common.Subject; 14 | 15 | import com.alibaba.fastjson.JSON; 16 | import com.alibaba.fastjson.JSONObject; 17 | 18 | /** 19 | * 默认安全认证主题实现(不可变对象) 20 | * @author yangyuan 21 | * @date 2017年4月26日 22 | */ 23 | public final class DefaultSubject implements Subject{ 24 | private static final String PRINCIPAL_SERIALIZABLE_NAME = "principal"; 25 | private static final String VALID_SERIALIZABLE_NAME = "valid"; 26 | 27 | private final Session session; 28 | private final String principal; 29 | private final boolean valid; 30 | 31 | /** 32 | * 指定principal的构造方法 33 | *

valid false

34 | *

session new

35 | * @param principal 唯一标识 36 | */ 37 | private DefaultSubject(String principal) { 38 | this.principal = principal; 39 | this.valid = false; 40 | this.session = new DefaultSession(); 41 | } 42 | 43 | /** 44 | * 指定principal、session的构造方法 45 | *

valid true

46 | * @param principal 唯一标识 47 | * @param session 会话,传null自动创建 48 | */ 49 | private DefaultSubject(String principal, Session session) { 50 | this.principal = principal; 51 | this.valid = true; 52 | if(session == null){ 53 | session = new DefaultSession(); 54 | } 55 | this.session = session; 56 | } 57 | 58 | /** 59 | * 完全自定义的构造方法 60 | * @param principal 唯一标识 61 | * @param valid 是否有效 62 | * @param session 会话,传null自动创建 63 | */ 64 | private DefaultSubject(String principal, boolean valid, Session session) { 65 | this.principal = principal; 66 | this.valid = valid; 67 | if(session == null){ 68 | session = new DefaultSession(); 69 | } 70 | this.session = session; 71 | } 72 | 73 | @Override 74 | public String getPrincipal() { 75 | return principal; 76 | } 77 | 78 | @Override 79 | public boolean isValid() { 80 | return valid; 81 | } 82 | 83 | @Override 84 | public Session getSession() { 85 | return session; 86 | } 87 | 88 | /** 89 | * 从请求对象中创建subject 90 | * @param request 请求对象 91 | * @return 如果请求中带有principal,则利用客户端指定的principal创建subject;如果找不到principal,则返回null 92 | */ 93 | public static DefaultSubject of(HttpServletRequest request){ 94 | Cookie[] cookies = request.getCookies(); 95 | if(cookies == null){ 96 | return null; 97 | } 98 | 99 | String clientPrincipal = null; 100 | for(Cookie cookie : cookies){ 101 | if(ResourceManager.cookie().getName().equals(cookie.getName())){ 102 | clientPrincipal = cookie.getValue(); 103 | break; 104 | } 105 | } 106 | if(StringUtils.isBlank(clientPrincipal)){ 107 | return null; 108 | } 109 | 110 | return new DefaultSubject(clientPrincipal); 111 | } 112 | 113 | /** 114 | * 标记subject状态为有效 115 | * @param subject 116 | * @return 如果传入的subject本身就是有效的,直接返回传入的subject;否则,会新建一个subject返回,新subject的principal、session与传入的subject一致 117 | */ 118 | public static DefaultSubject valid(DefaultSubject subject){ 119 | if(subject.isValid()){ 120 | return subject; 121 | } 122 | 123 | return new DefaultSubject(subject.getPrincipal(), subject.getSession()); 124 | } 125 | 126 | /** 127 | * 获取subject实例 128 | * @param principal 唯一标识 129 | * @param valid 是否有效 130 | * @param session 会话,传null自动创建 131 | * @return subject实例 132 | */ 133 | public static DefaultSubject getInstance(String principal, boolean valid, Session session){ 134 | return new DefaultSubject(principal, valid, session); 135 | } 136 | 137 | @Override 138 | public byte[] getBytes() { 139 | try { 140 | Map map = new HashMap(); 141 | map.put(PRINCIPAL_SERIALIZABLE_NAME, getPrincipal()); 142 | map.put(VALID_SERIALIZABLE_NAME, isValid()); 143 | String json = JSON.toJSONString(map); 144 | 145 | byte[] subjectBytes = json.getBytes(ResourceManager.core().getCharset()); 146 | byte[] sessionBytes = getSession().getBytes(); 147 | byte[] bytes = new byte[subjectBytes.length + sessionBytes.length + 8]; 148 | ByteBuffer buffer = ByteBuffer.wrap(bytes); 149 | buffer.clear(); 150 | buffer.putInt(subjectBytes.length); 151 | buffer.put(subjectBytes); 152 | buffer.putInt(sessionBytes.length); 153 | buffer.put(sessionBytes); 154 | 155 | return bytes; 156 | } catch (Exception e) { 157 | throw new SecurityException(e); 158 | } 159 | } 160 | 161 | /** 162 | * 反序列化 163 | * @param bytes 原始字节数组 164 | * @return subject实例 165 | */ 166 | public static DefaultSubject parse(byte[] bytes){ 167 | try { 168 | ByteBuffer buffer = ByteBuffer.wrap(bytes); 169 | buffer.clear(); 170 | 171 | int length = buffer.getInt(); 172 | byte[] subjectBytes = new byte[length]; 173 | buffer.get(subjectBytes); 174 | JSONObject subjectJson = JSON.parseObject(new String(subjectBytes, ResourceManager.core().getCharset())); 175 | 176 | length = buffer.getInt(); 177 | byte[] sessionBytes = new byte[length]; 178 | buffer.get(sessionBytes); 179 | DefaultSession session = DefaultSession.parse(sessionBytes); 180 | 181 | return DefaultSubject.getInstance(subjectJson.getString(PRINCIPAL_SERIALIZABLE_NAME), subjectJson.getBooleanValue(VALID_SERIALIZABLE_NAME), session); 182 | } catch (Exception e) { 183 | throw new SecurityException(e); 184 | } 185 | } 186 | 187 | @Override 188 | public String toString() { 189 | StringBuilder builder = new StringBuilder(); 190 | 191 | builder.append("#Subject\n"); 192 | 193 | builder.append("[principal]("); 194 | builder.append(getPrincipal()); 195 | builder.append(")\n"); 196 | 197 | builder.append("[valid]("); 198 | builder.append(isValid()); 199 | builder.append(")\n"); 200 | 201 | builder.append("#Session\n"); 202 | 203 | builder.append(getSession().toString()); 204 | 205 | return new String(builder); 206 | } 207 | 208 | } 209 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/config/CoreResource.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.config; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | import org.yangyuan.security.core.common.CacheManager; 6 | import org.yangyuan.security.core.common.ConcurrentSubjectControl; 7 | import org.yangyuan.security.core.common.PrincipalFactory; 8 | import org.yangyuan.security.core.common.SecurityAuthHandler; 9 | import org.yangyuan.security.core.common.SecurityManager; 10 | 11 | /** 12 | * 核心资源定义 13 | * @author yangyuan 14 | * @date 2018年3月31日 15 | */ 16 | public class CoreResource { 17 | /** 18 | * windows盘符正则 19 | */ 20 | private static final Pattern DISK_PATTERN = Pattern.compile("^/[a-zA-Z]:/"); 21 | /** 22 | * 全局统一编码 23 | */ 24 | private static final String UNIFY_CHARSET = "UTF-8"; 25 | 26 | /** 27 | * 应用classes目录绝对路径,以/结尾 28 | */ 29 | public static final String APP_CLASS_PATH; 30 | 31 | static { 32 | String path = CoreResource.class.getClassLoader().getResource("/").getPath(); 33 | if(DISK_PATTERN.matcher(path).find()){ 34 | path = path.replaceFirst("/", ""); 35 | } 36 | APP_CLASS_PATH = path; 37 | } 38 | 39 | /** 40 | * 全局统一编码 41 | */ 42 | private final String charset; 43 | /** 44 | * 应用classes目录绝对路径,以/结尾 45 | */ 46 | private final String appClassPath; 47 | /** 48 | * 是否使用客户端记录的subject登陆 49 | *
50 | * 如果设为true,客户端登陆时如果携带有subject信息,那么复用此subject,不再创建新的subject。 51 | *
52 | * 如果设为false,则登录时忽略客户端携带的subject信息,总是创建新的subject。 53 | */ 54 | private final boolean useClientSubjectLogin; 55 | /** 56 | * 安全管理器 57 | */ 58 | private final SecurityManager securityManager; 59 | /** 60 | * 安全唯一标识生成器 61 | */ 62 | private final PrincipalFactory principalFactory; 63 | /** 64 | * 缓存管理器 65 | */ 66 | private final CacheManager cacheManager; 67 | /** 68 | * 认证回调处理器 69 | */ 70 | private final SecurityAuthHandler securityAuthHandler; 71 | /** 72 | * 并发主题控制器 73 | */ 74 | private final ConcurrentSubjectControl concurrentSubjectControl; 75 | 76 | public CoreResource(Builder builder){ 77 | this.charset = builder.charset; 78 | this.appClassPath = builder.appClassPath; 79 | this.useClientSubjectLogin = builder.useClientSubjectLogin; 80 | this.securityManager = builder.securityManager; 81 | this.principalFactory = builder.principalFactory; 82 | this.cacheManager = builder.cacheManager; 83 | this.securityAuthHandler = builder.securityAuthHandler; 84 | this.concurrentSubjectControl = builder.concurrentSubjectControl; 85 | } 86 | 87 | public String getCharset() { 88 | return charset; 89 | } 90 | public String getAppClassPath() { 91 | return appClassPath; 92 | } 93 | public boolean isUseClientSubjectLogin() { 94 | return useClientSubjectLogin; 95 | } 96 | public SecurityManager getSecurityManager() { 97 | return securityManager; 98 | } 99 | public PrincipalFactory getPrincipalFactory() { 100 | return principalFactory; 101 | } 102 | public CacheManager getCacheManager() { 103 | return cacheManager; 104 | } 105 | public SecurityAuthHandler getSecurityAuthHandler() { 106 | return securityAuthHandler; 107 | } 108 | public ConcurrentSubjectControl getConcurrentSubjectControl() { 109 | return concurrentSubjectControl; 110 | } 111 | 112 | /** 113 | * 自定义核心资源构造器 114 | * @return 115 | */ 116 | public static Builder custom(){ 117 | return new Builder().charset(UNIFY_CHARSET) 118 | .appClassPath(APP_CLASS_PATH); 119 | } 120 | 121 | /** 122 | * 核心资源构造器 123 | * @author yangyuan 124 | * @date 2018年3月31日 125 | */ 126 | public static class Builder { 127 | /** 128 | * 全局编码 129 | */ 130 | private String charset; 131 | /** 132 | * 应用class目录 133 | */ 134 | private String appClassPath; 135 | /** 136 | * 是否使用客户端记录的subject登陆 137 | *
138 | * 如果设为true,客户端登陆时如果携带有subject信息,那么复用此subject,不再创建新的subject。 139 | *
140 | * 如果设为false,则登录时忽略客户端携带的subject信息,总是创建新的subject。 141 | */ 142 | private boolean useClientSubjectLogin; 143 | /** 144 | * 安全管理器 145 | */ 146 | private SecurityManager securityManager; 147 | /** 148 | * 安全唯一标识生成器 149 | */ 150 | private PrincipalFactory principalFactory; 151 | /** 152 | * 缓存管理器 153 | */ 154 | private CacheManager cacheManager; 155 | /** 156 | * 认证回调处理器 157 | */ 158 | private SecurityAuthHandler securityAuthHandler; 159 | /** 160 | * 并发主题控制器 161 | */ 162 | private ConcurrentSubjectControl concurrentSubjectControl; 163 | 164 | public Builder charset(String charset) { 165 | this.charset = charset; 166 | return this; 167 | } 168 | public Builder appClassPath(String appClassPath) { 169 | this.appClassPath = appClassPath; 170 | return this; 171 | } 172 | public Builder useClientSubjectLogin(boolean useClientSubjectLogin) { 173 | this.useClientSubjectLogin = useClientSubjectLogin; 174 | return this; 175 | } 176 | public Builder securityManager(SecurityManager securityManager) { 177 | this.securityManager = securityManager; 178 | return this; 179 | } 180 | public Builder principalFactory(PrincipalFactory principalFactory) { 181 | this.principalFactory = principalFactory; 182 | return this; 183 | } 184 | public Builder cacheManager(CacheManager cacheManager) { 185 | this.cacheManager = cacheManager; 186 | return this; 187 | } 188 | public Builder securityAuthHandler(SecurityAuthHandler securityAuthHandler) { 189 | this.securityAuthHandler = securityAuthHandler; 190 | return this; 191 | } 192 | public Builder concurrentSubjectControl(ConcurrentSubjectControl concurrentSubjectControl) { 193 | this.concurrentSubjectControl = concurrentSubjectControl; 194 | return this; 195 | } 196 | public CoreResource build(){ 197 | return new CoreResource(this); 198 | } 199 | } 200 | 201 | @Override 202 | public String toString() { 203 | StringBuilder builder = new StringBuilder(128); 204 | 205 | builder.append("[charset]("); 206 | builder.append(getCharset()); 207 | builder.append(")\n"); 208 | 209 | builder.append("[appClassPath]("); 210 | builder.append(getAppClassPath()); 211 | builder.append(")\n"); 212 | 213 | builder.append("[useClientSubjectLogin]("); 214 | builder.append(isUseClientSubjectLogin()); 215 | builder.append(")\n"); 216 | 217 | builder.append("[SecurityManager]("); 218 | if(getSecurityManager() == null){ 219 | builder.append("null"); 220 | }else{ 221 | builder.append(getSecurityManager().getClass().getName()); 222 | } 223 | builder.append(")\n"); 224 | 225 | builder.append("[PrincipalFactory]("); 226 | if(getPrincipalFactory() == null){ 227 | builder.append("null"); 228 | }else{ 229 | builder.append(getPrincipalFactory().getClass().getName()); 230 | } 231 | builder.append(")\n"); 232 | 233 | builder.append("[CacheManager]("); 234 | if(getCacheManager() == null){ 235 | builder.append("null"); 236 | }else{ 237 | builder.append(getCacheManager().getClass().getName()); 238 | } 239 | builder.append(")\n"); 240 | 241 | builder.append("[SecurityAuthHandler]("); 242 | if(getSecurityAuthHandler() == null){ 243 | builder.append("null"); 244 | }else{ 245 | builder.append(getSecurityAuthHandler().getClass().getName()); 246 | } 247 | builder.append(")\n"); 248 | 249 | builder.append("[ConcurrentSubjectControl]("); 250 | if(getConcurrentSubjectControl() == null){ 251 | builder.append("null"); 252 | }else{ 253 | builder.append(getConcurrentSubjectControl().getClass().getName()); 254 | } 255 | builder.append(")\n"); 256 | 257 | return new String(builder); 258 | } 259 | 260 | } 261 | -------------------------------------------------------------------------------- /src/main/java/org/yangyuan/security/realm/remote/RemoteRealm.java: -------------------------------------------------------------------------------- 1 | package org.yangyuan.security.realm.remote; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.yangyuan.security.bean.User; 5 | import org.yangyuan.security.config.ResourceManager; 6 | import org.yangyuan.security.core.QqRemoteToken; 7 | import org.yangyuan.security.core.RemoteToken; 8 | import org.yangyuan.security.core.WbRemoteToken; 9 | import org.yangyuan.security.core.WxRemoteToken; 10 | import org.yangyuan.security.core.common.SecurityToken; 11 | import org.yangyuan.security.exception.AuthRemoteFailException; 12 | import org.yangyuan.security.http.client.HttpClient; 13 | import org.yangyuan.security.http.response.SimpleResponse; 14 | import org.yangyuan.security.realm.bean.RemoteUser; 15 | import org.yangyuan.security.realm.bean.RemoteUserAdaptor; 16 | import org.yangyuan.security.realm.common.AbstractRealm; 17 | import org.yangyuan.security.realm.common.Realm; 18 | 19 | import com.alibaba.fastjson.JSON; 20 | import com.alibaba.fastjson.JSONObject; 21 | 22 | /** 23 | * 第三方数据源实现 24 | * @author yangyuan 25 | * @date 2018年3月15日 26 | */ 27 | public class RemoteRealm extends AbstractRealm{ 28 | 29 | /** 30 | * 第三方数据源具体实现列表 31 | *
32 | * 1 qq, 2 微信, 3 微博 33 | * 34 | */ 35 | private static final Realm[] REMOTE_REALMS = new Realm[]{ 36 | null, 37 | new QQRemoteRealm(), 38 | new WxRemoteRealm(), 39 | new WbRemoteRealm() 40 | }; 41 | 42 | /** 43 | * qq平台授权登陆 44 | * @author yangyuan 45 | * @date 2018年3月15日 46 | */ 47 | private static class QQRemoteRealm extends AbstractRealm{ 48 | 49 | @Override 50 | public User getUser(SecurityToken token) { 51 | QqRemoteToken remoteToken = (QqRemoteToken) token; 52 | 53 | try { 54 | /** 55 | * 获取openid 56 | */ 57 | String openidApi = "https://graph.qq.com/oauth2.0/me?access_token=" + remoteToken.getAccessToken() + "&unionid=1"; 58 | SimpleResponse response = HttpClient.getClient().get(openidApi); 59 | if(response.getCode() != 200){ 60 | throw new AuthRemoteFailException("无法获取openid,授权失败"); 61 | } 62 | String body = response.getStringBody(); 63 | if(StringUtils.isBlank(body)){ 64 | throw new AuthRemoteFailException("无法获取openid,授权失败"); 65 | } 66 | body = body.replaceAll("^\\s*callback\\s*\\(\\s*", ""); 67 | body = body.replaceAll("\\s*\\)\\s*;\\s*$", ""); 68 | JSONObject bodyJson = JSON.parseObject(body); 69 | if(bodyJson.containsKey("error")){ 70 | throw new AuthRemoteFailException(bodyJson.getString("error_description") + ",授权失败"); 71 | } 72 | String unionid = bodyJson.getString("unionid"); 73 | String openid = bodyJson.getString("openid"); 74 | String appid = bodyJson.getString("client_id"); 75 | 76 | /** 77 | * 获取用户信息 78 | */ 79 | String infoApi = "https://graph.qq.com/user/get_user_info?access_token=" 80 | + remoteToken.getAccessToken() + "&oauth_consumer_key=" 81 | + appid + "&openid=" + openid; 82 | response = HttpClient.getClient().get(infoApi); 83 | if(response.getCode() != 200){ 84 | throw new AuthRemoteFailException("无法获取用户信息,授权失败"); 85 | } 86 | body = response.getStringBody(); 87 | if(StringUtils.isBlank(body)){ 88 | throw new AuthRemoteFailException("无法获取用户信息,授权失败"); 89 | } 90 | bodyJson = JSON.parseObject(body); 91 | if(bodyJson.getIntValue("ret") != 0){ 92 | throw new AuthRemoteFailException("无法获取用户信息,授权失败"); 93 | } 94 | String nickname = bodyJson.getString("nickname"); 95 | String portrait = null; 96 | if(StringUtils.isNoneBlank(bodyJson.getString("figureurl_qq_1"))){ 97 | portrait = bodyJson.getString("figureurl_qq_1"); 98 | } 99 | if(StringUtils.isNoneBlank(bodyJson.getString("figureurl_qq_2"))){ 100 | portrait = bodyJson.getString("figureurl_qq_2"); 101 | } 102 | 103 | /** 104 | * 适配数据 105 | */ 106 | RemoteUser remoteUser = new RemoteUser(nickname, portrait, unionid); 107 | RemoteUserAdaptor userAdaptor = ResourceManager.dao().getRemoteRealmAdaptor().selectByRemoteUser(remoteUser); 108 | 109 | return getUser(userAdaptor.getUnionid(), userAdaptor.getRoles()); 110 | } catch (Exception e) { 111 | throw new AuthRemoteFailException(e.getMessage() + ",授权失败"); 112 | } 113 | } 114 | } 115 | 116 | /** 117 | * 微信平台授权登陆 118 | * @author yangyuan 119 | * @date 2018年3月15日 120 | */ 121 | private static class WxRemoteRealm extends AbstractRealm { 122 | 123 | @Override 124 | public User getUser(SecurityToken token) { 125 | WxRemoteToken remoteToken = (WxRemoteToken) token; 126 | 127 | try { 128 | String infoApi = "https://api.weixin.qq.com/sns/userinfo?access_token=" 129 | + remoteToken.getAccessToken() + "&openid=" 130 | + remoteToken.getOpenid(); 131 | SimpleResponse response = HttpClient.getClient().get(infoApi); 132 | if(response.getCode() != 200){ 133 | throw new AuthRemoteFailException("无法获取用户信息,授权失败"); 134 | } 135 | String body = response.getStringBody(); 136 | if(StringUtils.isBlank(body)){ 137 | throw new AuthRemoteFailException("无法获取用户信息,授权失败"); 138 | } 139 | JSONObject bodyJson = JSON.parseObject(body); 140 | if(bodyJson.containsKey("errcode")){ 141 | throw new AuthRemoteFailException(bodyJson.getString("errmsg") + ",授权失败"); 142 | } 143 | String unionid = bodyJson.getString("unionid"); 144 | String nickname = bodyJson.getString("nickname"); 145 | String portrait = bodyJson.getString("headimgurl").replace("\\", ""); 146 | 147 | /** 148 | * 访问适配器 149 | */ 150 | RemoteUser remoteUser = new RemoteUser(nickname, portrait, unionid); 151 | RemoteUserAdaptor userAdaptor = ResourceManager.dao().getRemoteRealmAdaptor().selectByRemoteUser(remoteUser); 152 | 153 | return getUser(userAdaptor.getUnionid(), userAdaptor.getRoles()); 154 | } catch (Exception e) { 155 | throw new AuthRemoteFailException(e.getMessage() + ",授权失败"); 156 | } 157 | } 158 | 159 | } 160 | 161 | /** 162 | * 微博平台授权登陆 163 | * @author yangyuan 164 | * @date 2018年3月15日 165 | */ 166 | private static class WbRemoteRealm extends AbstractRealm { 167 | 168 | @Override 169 | public User getUser(SecurityToken token) { 170 | WbRemoteToken remoteToken = (WbRemoteToken) token; 171 | 172 | try { 173 | String infoApi = "https://api.weibo.com/2/users/show.json?access_token=" + remoteToken.getAccessToken(); 174 | SimpleResponse response = HttpClient.getClient().get(infoApi); 175 | if(response.getCode() != 200){ 176 | throw new AuthRemoteFailException("无法获取用户信息,授权失败"); 177 | } 178 | String body = response.getStringBody(); 179 | if(StringUtils.isBlank(body)){ 180 | throw new AuthRemoteFailException("无法获取用户信息,授权失败"); 181 | } 182 | JSONObject bodyJson = JSON.parseObject(body); 183 | if(bodyJson.containsKey("error_code")){ 184 | throw new AuthRemoteFailException(bodyJson.getString("error") + ",授权失败"); 185 | } 186 | String unionid = String.valueOf(bodyJson.getLongValue("id")); 187 | String nickname = bodyJson.getString("screen_name"); 188 | String portrait = bodyJson.getString("avatar_large"); 189 | 190 | /** 191 | * 访问适配器 192 | */ 193 | RemoteUser remoteUser = new RemoteUser(nickname, portrait, unionid); 194 | RemoteUserAdaptor userAdaptor = ResourceManager.dao().getRemoteRealmAdaptor().selectByRemoteUser(remoteUser); 195 | 196 | return getUser(userAdaptor.getUnionid(), userAdaptor.getRoles()); 197 | } catch (Exception e) { 198 | throw new AuthRemoteFailException(e.getMessage() + ",授权失败"); 199 | } 200 | } 201 | 202 | } 203 | 204 | @Override 205 | public User getUser(SecurityToken token) { 206 | RemoteToken remoteToken = (RemoteToken) token; 207 | 208 | return REMOTE_REALMS[remoteToken.getPlanform()].getUser(token); 209 | } 210 | 211 | } 212 | --------------------------------------------------------------------------------