安全模块

用户描述类

/**
* 基本 Entity
*/
@Data
@MappedSuperclass
public class BaseEntity { /** Create time */
@Column(name = "create_time", columnDefinition = "timestamp default CURRENT_TIMESTAMP")
@Temporal(TemporalType.TIMESTAMP)
private Date createTime; /** Update time */
@Column(name = "update_time", columnDefinition = "timestamp default CURRENT_TIMESTAMP")
@Temporal(TemporalType.TIMESTAMP)
private Date updateTime; /** Delete flag */
@Column(name = "deleted", columnDefinition = "TINYINT default 0")
private Boolean deleted = false; /** 保存前被调用,初始化部分属性 */
@PrePersist
protected void prePersist() {
deleted = false;
Date now = DateUtils.now();
if (createTime == null) {
createTime = now;
} if (updateTime == null) {
updateTime = now;
}
} /** 更新前调用 */
@PreUpdate
protected void preUpdate() {
updateTime = new Date();
} /** 删除前调用 */
@PreRemove
protected void preRemove() {
updateTime = new Date();
}
} /**
* 用户 Entity
*/
@Data
@Entity
@Table(name = "users")
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class User extends BaseEntity { @Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id; @Column(name = "username", columnDefinition = "varchar(50) not null")
private String username; @Column(name = "nickname", columnDefinition = "varchar(255) not null")
private String nickname; @Column(name = "password", columnDefinition = "varchar(255) not null")
private String password; @Column(name = "email", columnDefinition = "varchar(127) default ''")
private String email; /** 头像 */
@Column(name = "avatar", columnDefinition = "varchar(1023) default ''")
private String avatar; /** 描述 */
@Column(name = "description", columnDefinition = "varchar(1023) default ''")
private String description; /** 生效时间(何时可以使用)*/
@Column(name = "expire_time", columnDefinition = "timestamp default CURRENT_TIMESTAMP")
@Temporal(TemporalType.TIMESTAMP)
private Date expireTime; @Override
public void prePersist() {
super.prePersist(); if (email == null) {
email = "";
} if (avatar == null) {
avatar = "";
} if (description == null) {
description = "";
} if (expireTime == null) {
expireTime = DateUtils.now(); //立即生效
}
}
} /**
* 用户描述类(封装用户)
*/
@ToString
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
public class UserDetail { private User user; @NonNull
public User getUser() {return user;} public void setUser(User user) {this.user = user;}
}

身份验证类

public interface Authentication {
@NonNull
UserDetail getDetail();
} public class AuthenticationImpl implements Authentication { private final UserDetail userDetail; public AuthenticationImpl(UserDetail userDetail) {this.userDetail = userDetail;} @Override
public UserDetail getDetail() {return userDetail;}
}

安全上下文类

public interface SecurityContext {

    @Nullable
Authentication getAuthentication(); void setAuthentication(@Nullable Authentication authentication); /** 检查是否已经验证过 */
default boolean isAuthenticated() {return getAuthentication() != null;}
} @ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class SecurityContextImpl implements SecurityContext { private Authentication authentication; @Override
public Authentication getAuthentication() {return authentication;} @Override
public void setAuthentication(Authentication authentication) {this.authentication = authentication;}
} public class SecurityContextHolder { private final static ThreadLocal<SecurityContext> CONTEXT_HOLDER = new ThreadLocal<>(); private SecurityContextHolder() {} /** 获取上下文 */
@NonNull
public static SecurityContext getContext() {
SecurityContext context = CONTEXT_HOLDER.get();
if (context == null) {
context = createEmptyContext();
CONTEXT_HOLDER.set(context);
}
return context;
} /** 设置上下文 */
public static void setContext(@Nullable SecurityContext context) {CONTEXT_HOLDER.set(context);} /** 清理上下文 */
public static void clearContext() {CONTEXT_HOLDER.remove();} /** 创建空的安全上下文 */
@NonNull
private static SecurityContext createEmptyContext() {return new SecurityContextImpl(null);}
}

身份验证Token

@Data
public class AuthToken { /** 访问令牌 */
@JsonProperty("access_token")
private String accessToken; /** 过期时间 */
@JsonProperty("expired_in")
private int expiredIn; /** 刷新令牌 */
@JsonProperty("refresh_token")
private String refreshToken;
}

Token缓存Key

public class SecurityUtils {

    private SecurityUtils() {
} //访问 Token Key(halo.admin.access_token.user.getId())
@NonNull
public static String buildAccessTokenKey(@NonNull User user) {
return ACCESS_TOKEN_CACHE_PREFIX + user.getId();
} //刷新 Token Key(halo.admin.refresh_token.user.getId())
@NonNull
public static String buildRefreshTokenKey(@NonNull User user) {
return REFRESH_TOKEN_CACHE_PREFIX + user.getId();
} // Token 访问 Key(halo.admin.access.token.accessToken)
@NonNull
public static String buildTokenAccessKey(@NonNull String accessToken) {
return TOKEN_ACCESS_CACHE_PREFIX + accessToken;
} // Token 刷新 Key(halo.admin.refresh_token.refreshToken)
@NonNull
public static String buildTokenRefreshKey(@NonNull String refreshToken) {
return TOKEN_REFRESH_CACHE_PREFIX + refreshToken;
}
}

SpringBoot自定义参数解析HandlerMethodArgumentResolver(实现HandlerMethodArgumentResolver接口)

public class AuthenticationArgumentResolver implements HandlerMethodArgumentResolver {

    public AuthenticationArgumentResolver() {
} /**
* 支持的参数(解析参数的类型:Authentication,UserDetail,User)
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> parameterType = parameter.getParameterType();
return (Authentication.class.isAssignableFrom(parameterType) ||
UserDetail.class.isAssignableFrom(parameterType) ||
User.class.isAssignableFrom(parameterType));
} @Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) { //获取参数类型
Class<?> parameterType = parameter.getParameterType(); //获取身份验证类(安全上下文没有身份验证类表示未登陆)
Authentication authentication = Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.orElseThrow(() -> new AuthenticationException("未登陆!")); //返回相应类型参数值(从安全上下文获取的身份验证信息)
if (Authentication.class.isAssignableFrom(parameterType)) {
return authentication;
} else if (UserDetail.class.isAssignableFrom(parameterType)) {
return authentication.getDetail();
} else if (User.class.isAssignableFrom(parameterType)) {
return authentication.getDetail().getUser();
} throw new UnsupportedOperationException("未知参数类型:" + parameterType);
}
} /**
* SpringBoot注册自定义参数处理器
*/
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthenticationArgumentResolver());
}

OncePerRequestFilter:一次请求只过滤一次

/**
* 身份验证过滤器抽象类
*/
public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter { protected final AntPathMatcher antPathMatcher; //Url地址匹配工具类
protected final HaloProperties haloProperties; //配置文件实体类
protected final OptionService optionService; //选项操作
private AuthenticationFailureHandler failureHandler; //失败处理器
/**
* 排除Url模板
*/
private Set<String> excludeUrlPatterns = new HashSet<>(2); protected AbstractAuthenticationFilter(HaloProperties haloProperties,
OptionService optionService) {
this.haloProperties = haloProperties;
this.optionService = optionService;
antPathMatcher = new AntPathMatcher();
} /**
* 从HttpServletRequest中获取Token
*/
@Nullable
protected abstract String getTokenFromRequest(@NonNull HttpServletRequest request); /** 身份验证 */
protected abstract void doAuthenticate(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException; /** 不应该过滤的请求 */
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
//anyMatch(T -> boolean):是否有元素符合匹配条件
return excludeUrlPatterns.stream().anyMatch(p -> antPathMatcher.match(p, request.getServletPath()));
} /**
* 添加排除Url模板
*
* String... excludeUrlPatterns:
* 本质上可变参数是一个数组。
* 一个方法只能有一个可变参数,并且需要放在最后。
*/
public void addExcludeUrlPatterns(@NonNull String... excludeUrlPatterns) {
Collections.addAll(this.excludeUrlPatterns, excludeUrlPatterns);
} /**
* 获取排除Url模板
*/
@NonNull
public Set<String> getExcludeUrlPatterns() {
return excludeUrlPatterns;
} /**
* 得到失败处理器
*/
@NonNull
protected AuthenticationFailureHandler getFailureHandler() {
if (failureHandler == null) {
synchronized (this) {
if (failureHandler == null) {
// Create default authentication failure handler
DefaultAuthenticationFailureHandler failureHandler = new DefaultAuthenticationFailureHandler();
failureHandler.setProductionEnv(haloProperties.isProductionEnv()); this.failureHandler = failureHandler;
}
}
}
return failureHandler;
} /**
* 设置失败处理器
*/
public void setFailureHandler(@NonNull AuthenticationFailureHandler failureHandler) {
this.failureHandler = failureHandler;
} /** 过滤 */
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//检查博客是否被安装
Boolean isInstalled = optionService.getByPropertyOrDefault(PrimaryProperties.IS_INSTALLED, Boolean.class, false); if (!isInstalled) {
getFailureHandler().onFailure(request, response, new NotInstallException("当前博客还没有初始化"));
return;
} if (shouldNotFilter(request)) {
filterChain.doFilter(request, response);
return;
} try {
//身份验证
doAuthenticate(request, response, filterChain);
} finally {
SecurityContextHolder.clearContext();
}
}
}

管理员身份验证过滤器

@Slf4j
public class AdminAuthenticationFilter extends AbstractAuthenticationFilter { /** Admin session key */
public final static String ADMIN_SESSION_KEY = "halo.admin.session"; /** Access token cache prefix */
public final static String TOKEN_ACCESS_CACHE_PREFIX = "halo.admin.access.token."; /** Refresh token cache prefix */
public final static String TOKEN_REFRESH_CACHE_PREFIX = "halo.admin.refresh.token."; /** 管理员Token请求头name */
public final static String ADMIN_TOKEN_HEADER_NAME = "ADMIN-" + HttpHeaders.AUTHORIZATION; /** 管理员Token请求参数name */
public final static String ADMIN_TOKEN_QUERY_NAME = "admin_token"; private final HaloProperties haloProperties; private final StringCacheStore cacheStore; private final UserService userService; public AdminAuthenticationFilter(StringCacheStore cacheStore,
UserService userService,
HaloProperties haloProperties,
OptionService optionService) {
super(haloProperties, optionService);
this.cacheStore = cacheStore;
this.userService = userService;
this.haloProperties = haloProperties;
} /** 身份验证 */
@Override
protected void doAuthenticate(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (!haloProperties.isAuthEnabled()) {
//如果当前用户存在,设置当前用户到安全上下文中,并通过过滤
userService.getCurrentUser().ifPresent(user ->
SecurityContextHolder.setContext(new SecurityContextImpl(new AuthenticationImpl(new UserDetail(user)))));
filterChain.doFilter(request, response);
return;
} //当前用户不存在,获取Token
String token = getTokenFromRequest(request); //Token不存在,表示未登陆
if (StringUtils.isBlank(token)) {
getFailureHandler().onFailure(request, response, new AuthenticationException("未登录,请登陆后访问"));
return;
} //从缓存中获取User Id
Optional<Integer> optionalUserId = cacheStore.getAny(SecurityUtils.buildTokenAccessKey(token), Integer.class); //没有User Id,表示Token过期
if (!optionalUserId.isPresent()) {
getFailureHandler().onFailure(request, response, new AuthenticationException("Token 已过期或不存在").setErrorData(token));
return;
} //获取User
User user = userService.getById(optionalUserId.get()); //构建 UserDetail
UserDetail userDetail = new UserDetail(user); //设置当前用户到安全上下文中,并通过过滤
SecurityContextHolder.setContext(new SecurityContextImpl(new AuthenticationImpl(userDetail)));
filterChain.doFilter(request, response);
} /** 获取Token */
@Override
protected String getTokenFromRequest(@NonNull HttpServletRequest request) { //从请求头中获取Token
String token = request.getHeader(ADMIN_TOKEN_HEADER_NAME); //如果请求头中Token是null,从参数中获取Token
if (StringUtils.isBlank(token)) {
token = request.getParameter(ADMIN_TOKEN_QUERY_NAME); log.info("从参数中获取Token:[{}:{}]", ADMIN_TOKEN_QUERY_NAME, token);
} else {
log.info("从请求头中获取Token:[{}:{}]", ADMIN_TOKEN_HEADER_NAME, token);
}
return token;
}
}

默认身份验证失败处理器

public class AuthenticationException extends HaloException {

    public AuthenticationException(String message) {
super(message);
} public AuthenticationException(String message, Throwable cause) {
super(message, cause);
} @Override
public HttpStatus getStatus() {
return HttpStatus.UNAUTHORIZED; //401:未授权
}
} @Slf4j
public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler { private boolean productionEnv = true; //生产环境(默认) //默认Json转换器
private ObjectMapper objectMapper = JsonUtils.DEFAULT_JSON_MAPPER; public DefaultAuthenticationFailureHandler() {
} @Override
public void onFailure(HttpServletRequest request, HttpServletResponse response, HaloException exception) throws IOException, ServletException {
log.warn("身份验证失败,ip:[{}]", ServletUtil.getClientIP(request)); BaseResponse<Object> errorDetail = new BaseResponse<>(); errorDetail.setStatus(exception.getStatus().value());
errorDetail.setMessage(exception.getMessage());
errorDetail.setData(exception.getErrorData()); if (!productionEnv) {
errorDetail.setDevMessage(ExceptionUtils.getStackTrace(exception));
} //设置响应类型:"application/json;charset=UTF-8"
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
//设置状态码
response.setStatus(exception.getStatus().value());
//打印响应体到前端
response.getWriter().write(objectMapper.writeValueAsString(errorDetail));
} /**
* 设置Json转换器
*/
public void setObjectMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
} /**
* 设置生产环境
*/
public void setProductionEnv(boolean productionEnv) {
this.productionEnv = productionEnv;
}
}

注册过滤器

@Configuration
public class HaloConfiguration { @Bean
public FilterRegistrationBean<AdminAuthenticationFilter> adminAuthenticationFilter(StringCacheStore cacheStore,
UserService userService,
HaloProperties haloProperties,
ObjectMapper objectMapper,
OptionService optionService) {
AdminAuthenticationFilter adminAuthenticationFilter = new AdminAuthenticationFilter(cacheStore,
userService, haloProperties, optionService); DefaultAuthenticationFailureHandler failureHandler = new DefaultAuthenticationFailureHandler();
failureHandler.setProductionEnv(haloProperties.isProductionEnv());
failureHandler.setObjectMapper(objectMapper); //排除路径
adminAuthenticationFilter.addExcludeUrlPatterns(
"/api/admin/login",
"/api/admin/refresh/*",
"/api/admin/installations",
"/api/admin/recoveries/migrations/*",
"/api/admin/is_installed",
"/api/admin/password/code",
"/api/admin/password/reset"
);
adminAuthenticationFilter.setFailureHandler(
failureHandler); FilterRegistrationBean<AdminAuthenticationFilter> authenticationFilter = new FilterRegistrationBean<>();
authenticationFilter.setFilter(adminAuthenticationFilter);
authenticationFilter.addUrlPatterns("/api/admin/*");
authenticationFilter.setOrder(1); return authenticationFilter;
}
}

Halo(八)的更多相关文章

  1. 使用Docker快速搭建Halo个人博客到阿里云服务器上[附加主题和使用域名访问]

    一.前言 小编买了一个服务器也是一直想整个网站,一直在摸索,看了能够快速搭建博客系统的教程.总结了有以下几种方式,大家按照自己喜欢的去搭建: halo wordpress hexo vuepress ...

  2. 如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成

    阅读目录 前言 建模 实现 结语 一.前言 前面几篇已经实现了一个基本的购买+售价计算的过程,这次再让售价丰满一些,增加一个会员价的概念.会员价在现在的主流电商中,是一个不大常见的模式,其带来的问题是 ...

  3. iOS可视化动态绘制八种排序过程

    前面几篇博客都是关于排序的,在之前陆陆续续发布的博客中,我们先后介绍了冒泡排序.选择排序.插入排序.希尔排序.堆排序.归并排序以及快速排序.俗话说的好,做事儿要善始善终,本篇博客就算是对之前那几篇博客 ...

  4. 我的MYSQL学习心得(八) 插入 更新 删除

    我的MYSQL学习心得(八) 插入 更新 删除 我的MYSQL学习心得(一) 简单语法 我的MYSQL学习心得(二) 数据类型宽度 我的MYSQL学习心得(三) 查看字段长度 我的MYSQL学习心得( ...

  5. Mina、Netty、Twisted一起学(八):HTTP服务器

    HTTP协议应该是目前使用最多的应用层协议了,用浏览器打开一个网站就是使用HTTP协议进行数据传输. HTTP协议也是基于TCP协议,所以也有服务器和客户端.HTTP客户端一般是浏览器,当然还有可能是 ...

  6. CRL快速开发框架系列教程八(使用CRL.Package)

    本系列目录 CRL快速开发框架系列教程一(Code First数据表不需再关心) CRL快速开发框架系列教程二(基于Lambda表达式查询) CRL快速开发框架系列教程三(更新数据) CRL快速开发框 ...

  7. 【Oracle 集群】Linux下Oracle RAC集群搭建之Oracle DataBase安装(八)

    Oracle 11G RAC数据库安装(八) 概述:写下本文档的初衷和动力,来源于上篇的<oracle基本操作手册>.oracle基本操作手册是作者研一假期对oracle基础知识学习的汇总 ...

  8. 八皇后算法的另一种实现(c#版本)

    八皇后: 八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例.该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于 ...

  9. C语言学习 第八次作业总结

    本次作业其实没有新的内容,主要就是复习上一次的一维数组的相关内容.冯老师布置了5道题目,其中涉及到一些比较简单的排序或者是查找的方法.因为数据很少,所以直接使用for循环遍历就可以了. 关于本次作业, ...

随机推荐

  1. MySQL体系结构概览

    MySQL体系结构 InnoDB体系结构 MySQL实例有一组后台线程.一些内存块和若干服务线程组成 在默认情况下,MySQL有7组后台线程,分别为1个主线程,4组IO线程,1个锁线程,1个错误监控线 ...

  2. 测开之路七十一:监控平台之js

    监控平台的js //datetimepicker的初始化函数(主要是对选择时间的下拉框)function init_datetimepicker() { //初始化格式和规则 $('#start'). ...

  3. JavaScript 表单验证正则表达式大全

    JavaScript 表单验证正则表达式大全[转载] 匹配中文字符的正则表达式: [u4e00-u9fa5] 评注:匹配中文还真是个头疼的事,有了这个表达式就好办了 匹配双字节字符(包括汉字在内):[ ...

  4. ICPC2019上海区域赛 部分题解(正在更新)

    K. Color Graph 题意: 给定一个简单图,点个数<=16,删去部分边后,使得该图中无边数为奇数得环,问剩下的边数最大为多少? 思路: 如果一个图中无奇数边的环,那么这个图一定是个二分 ...

  5. JS正则表达式校验金额

    //任意正整数,正小数(小数位不超过2位) var isNum=/^(([1-9][0-9]*)|(([0]\.\d{1,2}|[1-9][0-9]*\.\d{1,2})))$/; var num = ...

  6. git报错-Initial commit Untracked files nothing added to commit but untracked ……

    文章转自 https://www.jianshu.com/p/61c3db30d488 在目标执行命令 git stratus 报错 根据上面的文章,可以解决问题.不行的话,请留言. 感谢你的阅读

  7. Spring学习(七)--Spring MVC的高级技术

    一.Spring MVC配置的替代方案 我们已经了解如何通过AbstractAnnotationConfigDispatcherServlet- Initializer快速搭建了Spring MVC环 ...

  8. Codeforces 1097D (DP+分解质因数)

    题目 传送门 分析 考虑\(n=p^q\)且p为质数的情况 设dp[i][j]表示经过i次变化后数为\(p^j\)的概率 则初始值dp[0][q]=1 状态转移方程为\(dp[i][j]=\sum{} ...

  9. UML 类图关系(继承,实现,依赖,关联,聚合,组合)

    1.继承(is-a)      指的是一个类(称为子类.子接口)继承另外的一个类(称为父类.父接口)的功能.并能够添加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系:在Java中此类 ...

  10. http响应代码解释

    200:成功响应 302:找到,但是请求的资源在另外一个不同的url中. 400:错误请求.这个请求不能被服务器所理解,客户端必须修改请求. 401:未认证,这个请求需要用户认证. 404:未找到.服 ...