Jeepay 开源的支付框架
一 配置
WebSecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启@PreAuthorize @PostAuthorize 等前置后置安全校验注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired private UserDetailsService userDetailsService;//spring security 内置的用户账号密码验证
@Autowired private JeeAuthenticationEntryPoint unauthorizedHandler;
@Autowired private SystemYmlConfig systemYmlConfig;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 使用BCrypt强哈希函数 实现PasswordEncoder
* **/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(this.userDetailsService)
.passwordEncoder(passwordEncoder());
}
/** 允许跨域请求 **/
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
if(systemYmlConfig.getAllowCors()){
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true); //带上cookie信息
// config.addAllowedOrigin(CorsConfiguration.ALL); //允许跨域的域名, *表示允许任何域名使用
config.addAllowedOriginPattern(CorsConfiguration.ALL); //使用addAllowedOriginPattern 避免出现 When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.
config.addAllowedHeader(CorsConfiguration.ALL); //允许任何请求头
config.addAllowedMethod(CorsConfiguration.ALL); //允许任何方法(post、get等)
source.registerCorsConfiguration("/**", config); // CORS 配置对所有接口都有效
}
return new CorsFilter(source);
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 由于使用的是JWT,我们这里不需要csrf
.csrf().disable()
.cors().and()
// 认证失败处理方式 下面有代码
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
// 添加JWT filter
httpSecurity.addFilterBefore(new JeeAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
// 禁用缓存
httpSecurity.headers().cacheControl();
}
@Override
public void configure(WebSecurity web) throws Exception {
//ignore文件 : 无需进入spring security 框架
// 1.允许对于网站静态资源的无授权访问
// 2.对于获取token的rest api要允许匿名访问
web.ignoring().antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/**/*.png",
"/**/*.jpg",
"/**/*.jpeg",
"/**/*.svg",
"/**/*.ico",
"/**/*.webp",
"/*.txt",
"/**/*.xls",
"/**/*.mp4" //支持mp4格式的文件匿名访问
)
.antMatchers(
"/api/anon/**" //匿名访问接口
);
}
}
(1)为什么我需要手动注册AuthenticationManager?
AuthenticationManager 用于对用户进行身份验证和授权
https://www.it1352.com/1578556.html
代码使用的应该是第三种配置。
(2)
// 基于token,所以不需要session
// 所有的rest服务一定要设置为无状态,以提升操作效率和性能
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
JeeAuthenticationEntryPoint
身份认证失败 (上面配置有.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and())
@Component
public class JeeAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
// This is invoked when user tries to access a secured REST resource without supplying any credentials
// We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
//返回json形式的错误信息
// response.setCharacterEncoding("UTF-8");
// response.setContentType("application/json");
// response.getWriter().println("{\"code\":1001, \"msg\":\"Unauthorized\"}");
response.getWriter().flush();
}
}
JeeAuthenticationTokenFilter
// 添加JWT filter
httpSecurity.addFilterBefore(new JeeAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
前面是自己设置的配置类如下,后面通常是固定的 UsernamePasswordAuthenticationFilter
/**
* <p><b>Title: </b>JwtAuthenticationTokenFilter.java
* <p><b>Description: </b>
* spring security框架中验证组件的前置过滤器;
* 用于验证token有效期,并放置ContextAuthentication信息,为后续spring security框架验证提供数据;
* 避免使用@Component等bean自动装配注解:@Component会将filter被spring实例化为web容器的全局filter,导致重复过滤。
* @modify terrfly
* @version V1.0
* @site https://www.jeepay.vip
* @date 2021-04-27 15:50
* <p>
*/
public class JeeAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
JeeUserDetails jeeUserDetails = commonFilter(request);
if(jeeUserDetails == null){
chain.doFilter(request, response);
return;
}
//将信息放置到Spring-security context中
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(jeeUserDetails, null, jeeUserDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
private JeeUserDetails commonFilter(HttpServletRequest request){
String authToken = request.getHeader(CS.ACCESS_TOKEN_NAME);
if(StringUtils.isEmpty(authToken)){
authToken = request.getParameter(CS.ACCESS_TOKEN_NAME);
}
if(StringUtils.isEmpty(authToken)){
return null; //放行,并交给UsernamePasswordAuthenticationFilter进行验证,返回公共错误信息.
}
JWTPayload jwtPayload = JWTUtils.parseToken(authToken, SpringBeansUtil.getBean(SystemYmlConfig.class).getJwtSecret()); //反解析token信息
//token字符串解析失败
if( jwtPayload == null || StringUtils.isEmpty(jwtPayload.getCacheKey())) {
return null;
}
//根据用户名查找数据库
JeeUserDetails jwtBaseUser = RedisUtil.getObject(jwtPayload.getCacheKey(), JeeUserDetails.class);
if(jwtBaseUser == null){
RedisUtil.del(jwtPayload.getCacheKey());
return null; //数据库查询失败,删除redis
}
//续签时间
RedisUtil.expire(jwtPayload.getCacheKey(), CS.TOKEN_TIME);
return jwtBaseUser;
}
}
过滤器的步骤
1.根据每个请求的请求头中的iToken获得 authToken (用来保持sso的)
2.判断空 如果请求头没有 可以从请求参数中看有没有 没有返回null
3.自定义JWT工具包 解析authToken 和 Spring 上下文保存的单独的秘钥。
4.获得登录用户/创建时间/Redis缓存的key 的实体
5.根据Redis缓存的key中获得是否存在该用户 JeeUserDetails (这个类是implements UserDetails(springsecurity里面的))
6.存在的话就续约时间 自定义两小时左右。
SystemYmlConfig
/**
* 系统Yml配置参数定义Bean
*
* @author terrfly
* @site https://www.jeepay.vip
* @date 2021-04-27 15:50
*/
@Component
@ConfigurationProperties(prefix="isys")
@Data
public class SystemYmlConfig {
/** 是否允许跨域请求 [生产环境建议关闭, 若api与前端项目没有在同一个域名下时,应开启此配置或在nginx统一配置允许跨域] **/
private Boolean allowCors;
/** 生成jwt的秘钥。 要求每个系统有单独的秘钥管理机制。 **/
private String jwtSecret;
@NestedConfigurationProperty //指定该属性为嵌套值, 否则默认为简单值导致对象为空(外部类不存在该问题, 内部static需明确指定)
private OssFile ossFile;
/** 系统oss配置信息 **/
@Data
public static class OssFile{
/** 存储根路径 **/
private String rootPath;
/** 公共读取块 **/
private String publicPath;
/** 私有读取块 **/
private String privatePath;
}
}
JWTPayload
/*
* JWT payload 载体
* 格式:
{
"sysUserId": "10001",
"created": "1568250147846",
"cacheKey": "KEYKEYKEYKEY",
}
* @author terrfly
* @site https://www.jeepay.vip
* @date 2021/6/8 18:01
*/
@Data
public class JWTPayload {
private Long sysUserId; //登录用户ID
private Long created; //创建时间, 格式:13位时间戳
private String cacheKey; //redis保存的key
protected JWTPayload(){}
public JWTPayload(JeeUserDetails jeeUserDetails){
this.setSysUserId(jeeUserDetails.getSysUser().getSysUserId());
this.setCreated(System.currentTimeMillis());
this.setCacheKey(jeeUserDetails.getCacheKey());
}
/** toMap **/
public Map<String, Object> toMap(){
JSONObject json = (JSONObject)JSONObject.toJSON(this);
return json.toJavaObject(Map.class);
}
}
JWTUtils
/*
* JWT工具包
*
* @author terrfly
* @site https://www.jeepay.vip
* @date 2021/6/8 16:32
*/
public class JWTUtils {
/** 生成token **/
public static String generateToken(JWTPayload jwtPayload, String jwtSecret) {
return Jwts.builder()
.setClaims(jwtPayload.toMap())
//过期时间 = 当前时间 + (设置过期时间[单位 :s ] ) token放置redis 过期时间无意义
//.setExpiration(new Date(System.currentTimeMillis() + (jwtExpiration * 1000) ))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
/** 根据token与秘钥 解析token并转换为 JWTPayload **/
public static JWTPayload parseToken(String token, String secret){
try {
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
JWTPayload result = new JWTPayload();
result.setSysUserId(claims.get("sysUserId", Long.class));
result.setCreated(claims.get("created", Long.class));
result.setCacheKey(claims.get("cacheKey", String.class));
return result;
} catch (Exception e) {
return null; //解析失败
}
}
}
一切准备就绪之后 来到了登录接口了
/** 用户信息认证 获取iToken **/
@RequestMapping(value = "/validate", method = RequestMethod.POST)
@MethodLog(remark = "登录认证")
public ApiRes validate() throws BizException {
String account = Base64.decodeStr(getValStringRequired("ia")); //用户名 i account, 已做base64处理
String ipassport = Base64.decodeStr(getValStringRequired("ip")); //密码 i passport, 已做base64处理
String vercode = Base64.decodeStr(getValStringRequired("vc")); //验证码 vercode, 已做base64处理
String vercodeToken = Base64.decodeStr(getValStringRequired("vt")); //验证码token, vercode token , 已做base64处理
String cacheCode = RedisUtil.getString(CS.getCacheKeyImgCode(vercodeToken));
if(StringUtils.isEmpty(cacheCode) || !cacheCode.equalsIgnoreCase(vercode)){
throw new BizException("验证码有误!");
}
// 返回前端 accessToken
String accessToken = authService.auth(account, ipassport);
// 删除图形验证码缓存数据
RedisUtil.del(CS.getCacheKeyImgCode(vercodeToken));
return ApiRes.ok4newJson(CS.ACCESS_TOKEN_NAME, accessToken);
}
这里前端进行了base64加密,这里进行解密。
验证码是存储在redis 大概15s
接着开始认证账号密码
/**
* 认证
* **/
public String auth(String username, String password){
//1. 生成spring-security usernamePassword类型对象
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
//spring-security 自动认证过程;
// 1. 进入 JeeUserDetailsServiceImpl.loadUserByUsername 获取用户基本信息;
//2. SS根据UserDetails接口验证是否用户可用;
//3. 最后返回loadUserByUsername 封装的对象信息;
Authentication authentication = null;
try {
authentication = authenticationManager.authenticate(upToken);
} catch (JeepayAuthenticationException jex) {
throw jex.getBizException() == null ? new BizException(jex.getMessage()) : jex.getBizException();
} catch (BadCredentialsException e) {
throw new BizException("用户名/密码错误!");
} catch (AuthenticationException e) {
log.error("AuthenticationException:", e);
throw new BizException("认证服务出现异常, 请重试或联系系统管理员!");
}
JeeUserDetails jeeUserDetails = (JeeUserDetails) authentication.getPrincipal();
//验证通过后 再查询用户角色和权限信息集合
SysUser sysUser = jeeUserDetails.getSysUser();
//非超级管理员 && 不包含左侧菜单 进行错误提示
if(sysUser.getIsAdmin() != CS.YES && sysEntitlementMapper.userHasLeftMenu(sysUser.getSysUserId(), CS.SYS_TYPE.MCH) <= 0){
throw new BizException("当前用户未分配任何菜单权限,请联系管理员进行分配后再登录!");
}
// 查询当前用户的商户信息
MchInfo mchInfo = mchInfoService.getById(sysUser.getBelongInfoId());
if (mchInfo != null) {
// 判断当前商户状态是否可用
if (mchInfo.getState() == CS.NO) {
throw new BizException("当前商户状态不可用!");
}
}
// 放置权限集合
jeeUserDetails.setAuthorities(getUserAuthority(sysUser));
//生成token
String cacheKey = CS.getCacheKeyToken(sysUser.getSysUserId(), IdUtil.fastUUID());
//生成iToken 并放置到缓存
ITokenService.processTokenCache(jeeUserDetails, cacheKey); //处理token 缓存信息
//将信息放置到Spring-security context中
UsernamePasswordAuthenticationToken authenticationRest = new UsernamePasswordAuthenticationToken(jeeUserDetails, null, jeeUserDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationRest);
//返回JWTToken
return JWTUtils.generateToken(new JWTPayload(jeeUserDetails), systemYmlConfig.getJwtSecret());
}
authentication = authenticationManager.authenticate(upToken);
这里会跑到Spring Security 验证账号的地方
JeeUserDetailsServiceImpl
/**
* UserDetailsService实现类
*
* @author terrfly
* @modify zhuxiao
* @site https://www.jeepay.vip
* @date 2021-04-27 15:50
*/
@Service
public class JeeUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysUserAuthService sysUserAuthService;
/**
*
* 此函数为: authenticationManager.authenticate(upToken) 内部调用 ;
* 需返回 用户信息载体 / 用户密码 。
* 用户角色+权限的封装集合 (暂时不查询, 在验证通过后再次查询,避免用户名密码输入有误导致查询资源浪费)
*
* **/
@Override
public UserDetails loadUserByUsername(String loginUsernameStr) throws UsernameNotFoundException {
//登录方式, 默认为账号密码登录
Byte identityType = CS.AUTH_TYPE.LOGIN_USER_NAME;
if(RegKit.isMobile(loginUsernameStr)){
identityType = CS.AUTH_TYPE.TELPHONE; //手机号登录
}
//首先根据登录类型 + 用户名得到 信息
SysUserAuth auth = sysUserAuthService.selectByLogin(loginUsernameStr, identityType, CS.SYS_TYPE.MCH);
if(auth == null){ //没有该用户信息
throw JeepayAuthenticationException.build("用户名/密码错误!");
}
//用户ID
Long userId = auth.getUserId();
SysUser sysUser = sysUserService.getById(userId);
if (sysUser == null) {
throw JeepayAuthenticationException.build("用户名/密码错误!");
}
if(CS.PUB_USABLE != sysUser.getState()){ //状态不合法
throw JeepayAuthenticationException.build("用户状态不可登录,请联系管理员!");
}
return new JeeUserDetails(sysUser, auth.getCredential());
}
}
1.根据自己的规则返回数据库中验证存在的用户对象
通常数据库中不存放 原始密码 ,只存放 经过 前面加密算法加密后的 密码凭证
每次验证的过程是拿 前端传来的 密码根据相同的加密算法 和 数据库的 密码凭证比较 来验证的。
2.//验证通过后 再查询用户角色和权限信息集合
- 在Spring Security中权限和角色其实没有太大区分 都是放在同样一个
public List<SimpleGrantedAuthority> getUserAuthority(SysUser sysUser)
//用户拥有的角色集合 需要以ROLE_ 开头, 用户拥有的权限集合
List<String> roleList = sysRoleService.findListByUser(sysUser.getSysUserId());
List<String> entList = sysRoleEntRelaService.selectEntIdsByUserId(sysUser.getSysUserId(), sysUser.getIsAdmin(), sysUser.getSysType());
List<SimpleGrantedAuthority> grantedAuthorities = new LinkedList<>();
roleList.stream().forEach(role -> grantedAuthorities.add(new SimpleGrantedAuthority(role)));
entList.stream().forEach(ent -> grantedAuthorities.add(new SimpleGrantedAuthority(ent)));
return grantedAuthorities;
}
- 但是角色的话一定 ROLE_开头!!!!
3.生成token
cacheKey 根据自己的规则生成
String cacheKey = CS.getCacheKeyToken(sysUser.getSysUserId(), IdUtil.fastUUID());
4.生成iToken 并放置到缓存
ITokenService.processTokenCache(jeeUserDetails, cacheKey); //处理token 缓存信息
5.将信息放置到Spring-security context中
UsernamePasswordAuthenticationToken authenticationRest = new UsernamePasswordAuthenticationToken(jeeUserDetails, null, jeeUserDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationRest);
6.返回JWTToken
return JWTUtils.generateToken(new JWTPayload(jeeUserDetails), systemYmlConfig.getJwtSecret());
7.删除图形验证码缓存数据
RedisUtil.del(CS.getCacheKeyImgCode(vercodeToken));
8.返回accessToken 前端 保存。