文章目录
- 前言
- 模块结构
- 设计思路
- 配置
- maven配置
- WebSecurityConfigurerAdapter配置
- 重写configure(HttpSecurity http)
- 配置AuthenticationManager
- 认证实现
- 认证器的实现
- 数据库账号密码实现
- 钩子实现
- 加解密实现
- 授权实现
- 鉴权过滤器实现
- 签名实现
- 接口权限实现
- 如何使用FW-SECURITY
前言
前三张已经把应用框架的结构和最核心的Core模块已经分析完了,在实际开发中,我们只需要在我们的业务应用中引路核心maven最可以开始我们的业务开发了。Core处理了最基本的Web相关操作,如统一数据格式,统一异常,统一参数校验,统一上下文,解析器自定义;统一日志处理,统一日志(logback)配置;Mybatis配置封装,常用工具类;基础常量等实体;集成数据库版本管理(liqubase)定义了统一规范,也可灵活自定义;同时引入了nacos作为注册服务配置中心等。引入Core不论是单体应用还是微服务应用都能快速进行开发。读者也可以自己下载源码,进行自定义和扩展。
<dependency>
<groupId>com.mars.fw</groupId>
<artifactId>FW-CORE</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
不论是做web开发,还是app开发都绕不开一个问题,认证和授权;我们可以使用比较流行的框架Shiro或者Spring-security又或是自己定义途径很多;这边我使用的是基于spring-security来进行封装和自定义的。首先先来看下模块的结构:整个FW-SECURITY
分成认证和授权两大块:图中已经很清晰的展现了各个模块的主要实现;授权这一块如果是基于微服务,可以在网关层做。
模块结构
设计思路
spring-security框架已经帮我们把,请求的整个路径处理的很好了,在请求链路中有去多前置和后置的handler实现,可以使用默认也可以我们自定义;和勾子很像,我们可以很轻松的通过配置,在我们想要的节点定义过滤器或者handler来实现我们自己的逻辑。并且它能够通过配置解决跨域等问题。下面我用流程图的方式来说明:
配置
上面的图已经很清晰的展现了设计的思路,下面我们来看具体的实现。我将从配置说起,spring-security和spring-boot集成,所以我们只要简单的引入maven配置:
maven配置
<dependencies>
<dependency>
<groupId>com.mars.fw</groupId>
<artifactId>FW-CORE</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
</dependencies>
WebSecurityConfigurerAdapter配置
spring-security的所有配置都可以通过重写WebSecurityConfigurerAdapter的方法来配置。我们自定义一个类KingSecurityConfigure 继承WebSecurityConfigurerAdapter;接着我们来重写里面的部分方法。
重写configure(HttpSecurity http)
HttpSecurity对象是在WebSecurityConfigurerAdapter 的init的方法中获取的;具体的原理不在这边阐述,不然内容会太多,这边更多是以应用来阐述。下图是HttpSecurity和其他类的关系图,在init的方法中将HttpSecurity添加到securityFilterChainBuilders的list中,形成过滤器链。我们通过HttpSecurity对象可以像过滤链中注入我们自定义的过滤和各种配置,这个对象为我们提供了各种各样的入口
package com.mars.fw.security;
import com.mars.fw.security.authentication.filter.CustomAuthenticationFilter;
import com.mars.fw.security.authentication.handler.AuthFailureHandler;
import com.mars.fw.security.authentication.handler.AuthSuccessHandler;
import com.mars.fw.security.authentication.handler.CustomLogoutSuccessHandler;
import com.mars.fw.security.authentication.provider.MobilePasswordAuthenticationProvider;
import com.mars.fw.security.authentication.provider.SmsAuthenticationProvider;
import com.mars.fw.security.authentication.provider.UserPasswordAuthenticationProvider;
import com.mars.fw.security.authentication.service.CustomUserDetailsService;
import com.mars.fw.security.authorization.filter.AuthenticationFilter;
import com.mars.fw.security.tool.SecurityConstant;
import com.mars.fw.security.tool.model.password.MD5PasswordEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.util.Arrays;
/**
* @Author King
* @create 2020/5/6 15:28
*/
@Configuration
@EnableWebSecurity
public class KingSecurityConfigure extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Bean
PasswordEncoder passwordEncoder() {
return new MD5PasswordEncoder();
}
@Bean
SmsAuthenticationProvider smsAuthenticationProvider() {
return new SmsAuthenticationProvider();
}
@Bean
MobilePasswordAuthenticationProvider emailAuthenticationProvider() {
return new MobilePasswordAuthenticationProvider();
}
@Bean
AuthenticationProvider customDaoAuthenticationProvider() {
return new UserPasswordAuthenticationProvider(passwordEncoder(), userDetailsService);
}
public CustomAuthenticationFilter authenticationFilter() throws Exception {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());
filter.setAuthenticationSuccessHandler(new AuthSuccessHandler());
filter.setAuthenticationFailureHandler(new AuthFailureHandler());
return filter;
}
@Override
protected AuthenticationManager authenticationManager() throws Exception {
ProviderManager providerManager = new ProviderManager(Arrays.asList(customDaoAuthenticationProvider(), emailAuthenticationProvider(), smsAuthenticationProvider()));
providerManager.setEraseCredentialsAfterAuthentication(false);
return providerManager;
}
/**
* 安全认证配置
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginProcessingUrl(SecurityConstant.LOGIN_ACTION)
.and()
.logout()
.logoutUrl(SecurityConstant.LOGOUT_ACTION)
.logoutSuccessHandler(new CustomLogoutSuccessHandler())
.and()
.authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new AuthenticationFilter(authenticationManager()));
}
}
- cors() 允许跨域处理,然后我们在新建一个配置类在里面注入CorsConfigurationSource的Bean,SpringSecurity会自动寻找name=corsConfigurationSource的Bean
- csrf() CSRF攻击处理
- addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
在我们定义的authenticationFilter()在UsernamePasswordAuthenticationFilter过滤器之前执行 - formLogin() 表单登录
- loginProcessingUrl(SecurityConstant.LOGIN_ACTION) 登录的接口地址
- logout() 允许注销
- logoutUrl(SecurityConstant.LOGOUT_ACTION) 注销调用的地址
- logoutSuccessHandler(new CustomLogoutSuccessHandler()) 注销成功的回调 在里面处理注销后的逻辑
- authorizeRequests().antMatchers("/**").permitAll().anyRequest().authenticated() 匹配?/下的所有请求都需要走认证接口
- addFilter(new AuthenticationFilter(authenticationManager())) 在过滤器链中加入鉴权的过滤器,都会先及格过这个过滤器
配置AuthenticationManager
在spring-security的过滤器链路中,当拦截到登录接口的时候,它定义了一套类似责任链的模式来管理认证的数据流;除了最常用的UserDetailService的方式外。spring-security还定义了AuthenticationManager 来管理所有的登录方式,我们可以自定义各种provider来实现不同的认证逻辑,然后通过AuthenticationManager来统一管理,AuthenticationManager可以将所有的provider管理起来,可以定义只要其中一种provider通过认证就认证成功,否则按一定顺序找下一个provider,知道找到成功为止,或者全部失败则认证失败。原理可以在后续单独将这一块在来分析。
这边我定义了三个handler,具体的实现可以在源码中参考。
认证实现
认证器的实现
上面讲到我定义了三个provider,分别是短信登录,用户名密码登录,手机密码登录,只要满足其中一种就可以登录。
我以现在最常用的短信登录为例子:SmsAuthenticationProvider,可以看到实际上是继承了一个AbstractAuthenticationProvider
AbstractAuthenticationProvider这个了也是我定义的做认证的公共实现。在虚类里面实现AuthenticationProvider接口,这个类里面有两个方法authenticate(Authentication authentication)和boolean supports(Class<?> authentication)
authenticate:是我们认证逻辑的实现
supports:自定义逻辑来开启是否开启这个认证逻辑,返回true的时候才会开启这个认证
package com.mars.fw.security.authentication.provider;
import com.mars.fw.security.tool.model.CustomAuthenticationToken;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
/**
* @description:定义一个自定义的provider的虚类做认证的公共实现
* @author: aron
* @date: 2019-07-03 15:45
*/
public abstract class AbstractAuthenticationProvider implements AuthenticationProvider {
private CustomAuthenticationToken customAuthenticationToken;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
customAuthenticationToken = (CustomAuthenticationToken) authentication;
return authenticateCustom(customAuthenticationToken);
}
@Override
public boolean supports(Class<?> authentication) {
return true;
}
/**
* 自定义认证方法 由子类实现
*
* @param customAuthenticationToken 自定义参数
* @return 返回认证的结果 传递到链路中
*/
protected abstract Authentication authenticateCustom(CustomAuthenticationToken customAuthenticationToken);
}
我们看下具体的实现,authenticateCustom 我们只要实现父类中的这个模板方法,里面的逻辑就不用多讲了短信验证的一个逻辑如果大家要使用短信,需要自己实现短信服务,后续在微服务中也会提供短息服务。
package com.mars.fw.security.authentication.provider;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.additional.query.impl.LambdaQueryChainWrapper;
import com.mars.fw.cache.CacheService;
import com.mars.fw.common.utils.SpringUtil;
import com.mars.fw.common.utils.StringUtils;
import com.mars.fw.security.authentication.exception.AuthenticationSmsException;
import com.mars.fw.security.authentication.service.CustomUserDetails;
import com.mars.fw.security.entity.UserEntity;
import com.mars.fw.security.mapper.KingUserMapper;
import com.mars.fw.security.tool.model.CustomAuthenticationToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
* @description: 自定义短信验证码登录的provider
* @author: aron
* @date: 2019-04-20 09:46
*/
public class SmsAuthenticationProvider extends AbstractAuthenticationProvider {
public static String SMS_CODE = "sms_code";
@Autowired
private KingUserMapper kingUserMapper;
@Override
protected Authentication authenticateCustom(CustomAuthenticationToken customAuthenticationToken) {
try {
String phone = (String) customAuthenticationToken.getPrincipal();
String smsCode = (String) customAuthenticationToken.getCredentials();
UserEntity userEntity = kingUserMapper.selectOne(new LambdaQueryWrapper<UserEntity>().eq(UserEntity::getPhone, phone));
if (userEntity == null) {
throw new UsernameNotFoundException("手机用户不存在或该手机未绑定用户!");
}
//从缓存中获取手机号的短信验证码
CacheService cacheService = SpringUtil.getBean(CacheService.class);
Object object = cacheService.get(SMS_CODE + "_" + phone);
if (null == object) {
throw new AuthenticationSmsException("短信验证码已过期");
}
if (!smsCode.equals(object.toString())) {
throw new AuthenticationSmsException("短信验证码错误");
}
//密码校验通过后 构建数据库用户信息
CustomUserDetails userAccountDetails = new CustomUserDetails(userEntity);
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userAccountDetails, customAuthenticationToken.getCredentials(), null);
return result;
} catch (Exception e) {
throw new AuthenticationSmsException("手机短信验证登录失败");
}
}
}