# kduck-security **Repository Path**: lhg317/kduck-security ## Basic Information - **Project Name**: kduck-security - **Description**: K-Duck-Security安全模块是基于kduck-core、Spring Security封装的一套鉴权框架,提供传统的标准认证、授权功能,同时支持OAuth2的鉴权,方便建立基于OAuth2的单点登陆架构。在模块中提供了便捷的使用方式简化复杂的鉴权过程,以及提供了灵活的扩展方式,应对各种安全要求场景。对于认证提供了一套常用的安全管控功能,用于保护用户的账户安全。 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: PR-1 - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2020-02-05 - **Last Updated**: 2023-09-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # K-Duck-Security # 框架介绍 K-Duck-Security安全模块是基于kduck-core、Spring Security封装的一套鉴权框架,提供传统的标准认证、授权功能,同时支持OAuth2的鉴权,方便建立基于OAuth2的单点登陆架构。在模块中提供了便捷的使用方式简化复杂的鉴权过程,以及提供了灵活的扩展方式,应对各种安全要求场景。对于认证提供了一套常用的安全管控功能,用于保护用户的账户安全。 # 框架底层技术 1. JDK 8 2. Kduck-Core-1.1.0 3. Spring-Security # 框架使用 我们以一个简单的认证授权示例来演示K-Duck安全框架的使用。 ## 鉴权流程 假设我们需要进行认证的流程如下图所示: ##开发步骤 安全模块的配置及使用主要有以下几步: 1. 配置依赖,安全模块是独立于kduck-core之外的模块。 2. 编写认证用户查询接口实现,用于用户登录认证的处理逻辑。 3. 编写用户授权接口实现,判断用户是否有权限调用指定接口请求。 4. 编写Controller业务接口类,用于演示受保护资源权限中定义的资源。 5. 准备登录失败及成功后的页面。 6. 配置相关参数。 7. 运行测试。 ###配置依赖 K-Duck安全模块需要单独引入pom依赖: ```xml cn.kduck kduck-core 1.1.0 cn.kduck kduck-security 1.1.0 ``` kduck-security模块需要依赖kduck-core核心模块。 ###编写认证用户查询接口实现 认证用户查询接口负责在登录时根据用户提供的用户(登录)名查询用户包含密码在内的基本信息,用于交给框架进行登录凭证的校验。 认证用户查询接口直接使用Spring Security提供的org.springframework.security.core.userdetails.UserDetailsService接口即可, 实现接口中的loadUserByUsername(String username)方法,username参数为用户登录时填写的用户(登录)名,例如下面的示例: ```java @Component public class UserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if("liuhg".equals(username)){ return new AuthUser("1",username,"$2a$10$zV5BmOgZrvFafnUL2iXVnumw2mi26GIh1nC4o3nAigDI5kQsmUk3e", Collections.emptyList()); } throw new UsernameNotFoundException("登录名不存在,username=" + username); } } ``` 代码中判断登录名是否等于“liuhg”,如果是则返回用户的授权对象AuthUser,实际使用时,这里应该是根据用户名查询账户信息并返回,AuthUser构造参数的含义: - 参数1:用户账户的ID - 参数2:登录名,即是方法参数提供的用户名。 - 参数3:登录密码,框架对密码使用BCryptPasswordEncoder类进行编码后保存,所以这里提供的是编码后的密码。 - 参数4:授予的权限,一般存放登录用户授权校验的凭据,比如角色编码等。这里为了演示,只给了一个空List集合。 如果登录名不等于“liuhg”,模拟用户不存在的场景,所以抛出用户名不存在的UsernameNotFoundException异常,这个异常抛出后不会体现到终端,而是会触发登录失败事件,一般是跳转回登录页面。 在这个接口实现中,不处理登录的认证,只需要返回用户名对应的基本用户信息即可,认证主要需要登录名和密码,因此不用返回过多的数据信息。 最后将这个接口实现类声明为一个Spring的Bean即可自动装配。 ###编写用户授权接口实现 用户的授权是当用户经过登录认证之后,判断某个资源接口是否允许当前已登录用户访问(即是否有权限)的逻辑类。 要完成授权功能,需要开发者自己实现cn.kduck.security.RoleAccessVoter接口的vote(Authentication authentication, Object object, Collection collection)方法,参数说明如下: - 参数1:认证对象,访问受保护资源时,这个认证对象包含在认证时封装的AuthUser对象。 - 参数2:一般是一个FilterInvocation对象,从中可以获取到HttpServletRequest和HttpServletResponse对象。 - 参数3:当前请求配置的所需授权,比如:permitAll、authenticated、denyAll、anonymous、rememberMe或者其他表达式。 该方法返回一个int值表示授权成功与否,使用接口中提供的常量返回: - int ACCESS_GRANTED = 1; //允许访问 - int ACCESS_ABSTAIN = 0;//弃权 - int ACCESS_DENIED = -1; //拒绝访问 其中返回0是弃权,这是什么意思呢?是因为访问授权决策机制是投票形式的,原则上Spring允许有多个接口实现依次进行授权投票,根据允许或拒绝的票数决定(默认有3种策略:全票通过、多数通过、一票通过)是否放权,因此过程中也有弃权的投票。但,框架默认只装配了一个投票决策器且策略为“一票通过”,返回0和1的效果是一样的。 为了方便实现,框架提供了一个AbstractRoleAccessVoter抽象类,该抽象类实现了RoleAccessVoter接口,同时要求继承类实现必要的两个方法,下面是一个简单示例: ```java @Component public class CustomRoleAccessVoter extends AbstractRoleAccessVoter { /** * 查询当前用户所属角色的所保护资源 * @param roleCodes 用户所属的角色编码 * @return 被保护的资源 */ @Override public List listResourceOperateByCode(String[] roleCodes) { return Arrays.asList(new ProtectedResource("/user/list","GET")); } /** * 查询所有被保护的资源 * @return 被保护的资源 */ @Override public List listAllResourceOperate() { return Arrays.asList(new ProtectedResource("/user/list","GET"),new ProtectedResource("/organization/list","GET")); } } ``` - listResourceOperateByCode:查询当前用户所属角色的所有保护的资源,包含路径和请求Method,路径可以使用通配符,每次请求都会进行调用,保证实时的权限变化响应。roleCodes这个参数是哪来的呢?这个就是在认证的时候AuthUser对象的最后一个构造参数,上面认证代码示例中给了一个空集合。 - listAllResourceOperate:查询所有被保护的资源对象,这个接口返回数据会被缓存,仅会被调用一次。 示例代码的简单说明,代码没有真正意义上使用到roleCodes角色编码,而是直接返回了含有一个“/user/list”的受保护资源,意思是所有登录后的用户都可以以“GET”方式访问“/user/list”接口,然后所有系统中只有2个受保护资源“/user/list”和“/user/add”,其余其他的所有接口均不会进行授权控制,即均允许访问。最后同样别忘记将这个实现类声明为Spring的Bean。 ###编写Controller业务接口类 这一步我们创建一个Controller来模拟受保护的资源,代码如下: ```java @RestController @RequestMapping public class DemoController { @GetMapping("/user/list") public JsonObject userList(){ return new JsonObject("用户列表"); } @GetMapping("/organization/list") public JsonObject orgList(){ return new JsonObject("机构列表"); } } ``` ###准备登录失败及成功后的页面 为了更加直观的演示鉴权效果,我们准备3个页面用来显示鉴权成功和失败的情况:成功后跳转的页面:index.html,登录失败的页面:loginFail.html,授权失败页:unauthorized.html,将这些页面放到SpringBoot默认定义的资源目录static中。在两个页面分别输出一句话: - index.html:welcome - loginFail:login fail - unauthorized.html:unauthorized ###配置相关参数 在原有application.yml上增加认证授权参数配置,目前框架对于标准认证授权提供以下的参数: ```yaml kduck: security: defaultSuccessUrl: /index.html defaultFailureUrl: /loginFail.html accessDeniedUrl: /unauthorized.html successUrlParameter: successUrl alwaysUse: false loginPage: /index.html httpBasic: true ignored: /xxx.html,/yyy.html ``` - defaultSuccessUrl:登录后默认的跳转路径,一般配合alwaysUse使用。 - successUrlParameter:登录后,根据参数指定的值来进行跳转。参数挂在处理登录的请求上,例如:/login?successParam=/index.html - alwaysUse:是否登录成功后总是跳转到defaultSuccessUrl指定的路径 - loginPage:默认的登录页路径,如果未配置loginPage参数,则默认使用SpringSecurity自带的登录页面。 - httpBasic:是否启用Basic认证,默认为true。 - ignored:不需鉴权的路径,多个的话以逗号分隔。 index.html我们不配置,使用Spring默认的机制,即直接访问index.html,登录成功后会跳转回登录到之前请求的地址index.html。 ###运行测试 首先确保之前步骤创建的认证和授权类能够成功被Spring加载,然后启动运行应用,我们期待的效果是: - 访问index.html,跳转到登录页面 - 输入正确的账号信息后成功访问并显示index.html中的内容。 - 输入错误的账号信息后会显示loginFail.html中的内容。 - 登录成功后访问/user/list,可以成功看到返回信息 - 登录成功后访问/user/list,返回信息unauthorized.html中的信息 首先我们依照步骤开始访问系统http://127.0.0.1:8080/index.html会转到SpringSecurity的登录页面: ![输入图片说明](https://images.gitee.com/uploads/images/2021/0109/190235_25ec4978_403814.png "登录页面.png") 输入错误的账号,点击登录后会看到失败页面: ![输入图片说明](https://images.gitee.com/uploads/images/2021/0109/190304_b4f3463c_403814.png "登录失败.png") 输入成功的账号“liuhg/111111”,登录后可以看到成功页面: ![输入图片说明](https://images.gitee.com/uploads/images/2021/0109/190340_4a728bf2_403814.png "成功页面.png") 此时我们访问http://127.0.0.1:8080/user/list,会正常返回数据: ![输入图片说明](https://images.gitee.com/uploads/images/2021/0109/190354_16c016db_403814.png "正常返回数据.png") 然后我们访问http://127.0.0.1:8080/organization/list,请求会被拒绝 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0109/190418_2bd54e0f_403814.png "拒绝访问.png") 这里会被拦截的原因是因为在授权决策器CustomRoleAccessVoter中,listResourceOperateByCode方法中,我们只为用户分配了“/user/list”的权限,如果想访问“/organization/list”接口,我们需要将其配置到返回结果中,当然还有一种方式就是从listAllResourceOperate方法的返回结果中删除“/organization/list”相关的资源,让其成为一个公开资源,而非保护资源。