文章目录
Codingmore学习
Codingmore项目结构
-
codingmore-admin,后台管理系统接口
-
codingmore-common,工具类及通用代码
-
codingmore-mbg, MyBatis-Plus 生成的数据库操作代码
-
codingmore-security:SpringSecurity封装公用模块
-
codingmore-web:前台展示系统接口
Codingmore-admin
codingmore-admin
├─component
├─config
├─controller
└─service
└─impl
componentPublishPostJob定时发布文章
configCodingmoreSecurityConfig该Java类定义了两个Bean:userDetailsService和dynamicSecurityService。- userDetailsService:根据用户名加载用户的详细信息,用于认证。
- dynamicSecurityService:从数据库中获取资源列表,并将其转换为安全配置属性,用于授权访问控制。具体来说,它将每个资源的URL与对应的权限关联起来,存储在一个并发哈希映射中。
GlobalCorsConfig全局跨域配置MybatisConfig用于配置MyBatis相关功能,具体功能如下:- 通过
@Configuration和@EnableTransactionManagement注解声明此为Spring配置类,并开启事务管理。使用@MapperScan注解指定MyBatis映射接口的位置。 - 定义了
mybatisPlusInterceptor方法,创建并返回一个MybatisPlusInterceptor实例,用于添加分页拦截器PaginationInnerInterceptor,确保在MySQL数据库环境下支持分页查询。
- 通过
OssCientConfig定义了一个配置类,用于创建阿里云OSS客户端。功能如下:- 通过
@Value注解从配置文件中读取OSS服务的endpoint、accessKeyId和accessKeySecret。使用@Bean注解创建并返回一个OSSClient实例,以便应用程序使用该客户端与阿里云OSS服务交互。
- 通过
SwaggerConfig配置了Swagger文档,主要功能如下:- 定义一个Docket类型的bean,指定API文档的生成规则、启用状态及安全方案。设置API的基本信息,如标题、描述、联系人和版本号。配置安全方案,定义请求头中的认证信息。指定需要认证的API路径。设置默认认证范围。
ThreadPoolConfig定义了一个线程池配置。具体功能如下:- 从配置属性中读取线程池的核心参数:核心线程数、最大线程数、队列容量等。
- 定义一个名为ossUploadImageExecutor的线程池执行器,并设置上述参数。
- 配置拒绝策略为CallerRunsPolicy,即当任务队列满时,调用者会执行该任务。
- 设置线程池关闭时等待所有任务完成。
WebConfigBeans添加了自定义的日期转换器
controller- controller不和数据库中的表一一对应,而是与业务对应
CommentsControllerLinksControllerPostTagRelationControllerTermRelationshipsController中不包含业务逻辑UserControllerCRUD,禁用用户,返回token,刷新token,获取当前登录用户信息,登出,修改密码,获取角色,分配角色MenuControllerCRUD,返回所有菜单树状图MinIOController只包含一个上传接口OssController一个上传接口PostsControllerCRUD, 添加文章栏目关联,设置和取消置顶,上传PostTagControllerCRUD, 模糊匹配,分页查询ResourceCategoryControllerCRUDResourceControllerCRUDRoleControllerCRUD,分页,获取或分配角色相关的菜单、资源SiteControllerCRUDTermTaxonomyController栏目CRUD,查找子栏目
service- 包含数据库16个表的Service接口和Oss服务的接口
Impl包含所有接口的实现类
Codingmore-common
codingmore-common
├─assist
├─component
├─exception
├─state
├─util
└─webapi
codingmore-common
-
assist:RedisConstants类为Java应用程序提供了一组常量和方法,用于处理Redis数据库中的网页浏览量、帖子点赞数以及管理员用户和资源信息。
-
componentBindingResultAspect这段代码是一个使用Spring AOP(面向切面编程)的切面类,用于处理Hibernate Validator(一个Java验证框架)的错误结果。DateConverter日期转换类,将标准日期、标准日期时间、时间戳转换成Date类型WebLogController层的日志封装类,包含了日志的属性WebLogAspect统一日志处理切面,记录请求信息,包括请求的URL、方法、参数、结果等。
-
exceptionApiException继承 RuntimeExceptionAsserts断言处理,用于抛出API异常GlobalExceptionHandler全局异常处理
-
state- 包含一些枚举类
PostStatusUserStatus包含文章和用户状态PostTypeTermRelationTypeUserType用于区别类型
-
utilFileNameUtil用于生成文件名称,主要是图片名称,用于OSS服务和MinIO
-
webapiIErrorCode接口类,封装API的错误码ResultCodeIErrorCode的实现类,包含可能用到的操作码ResultObject通用的返回对象类
全局异常处理
逻辑:参数校验出现异常时抛出,进行捕获
- 新建一个自定义异常类 ApiException
- 新建一个断言处理类Asserts,失败时抛出ApiException异常
- 新建一个全局异常处理类GlobalExceptionHandler, 当捕获到ApiException时,检查异常对象中的errorCode是否为空。如果errorCode不为空,则使用它构建失败的ResultObject返回。
否则,使用异常消息构建失败的ResultObject返回。
Codingmore-mbg
dto:数据传输对象vo: 封装数据便于在视图中展示service: 包含了Redis操作的接口和实现。数据库表对应的Service不在此模块中。mapper:映射接口,定义了与数据库交互的接口。model: 相当于entity,其中每一个类对应一个数据库中的表。config:RedisConfig redis配置类。util: 代码生成工具,日期工具。
Codingmore-security
Codingmore后台登录认证
使用SpringSecurity + JWT,将代码封装在codingmore-security中
codingmore-security
├── component
| ├── JwtAuthenticationTokenFilter -- JWT登录授权过滤器
| ├── RestAuthenticationEntryPoint
| └── RestfulAccessDeniedHandler
├── config
| ├── IgnoreUrlsConfig
| └── SecurityConfig
└── util
└── JwtTokenUtil -- JWT的token处理工具类
-
JwtAuthenticationTokenFilter
-
JWT登录授权过滤器,每次有请求时,从请求头中获取JWT。
-
从JWT中解析出username,并验证JWT的时效。
-
校验成功,将token放在ThreadLocal中
-
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class); @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 从客户端请求中获取 JWT String authHeader = request.getHeader(this.tokenHeader); // 该 JWT 是我们规定的格式,以 tokenHead 开头 if (authHeader != null && authHeader.startsWith(this.tokenHead)) { // The part after "Bearer " String authToken = authHeader.substring(this.tokenHead.length()); // 从 JWT 中获取用户名 String username = jwtTokenUtil.getUserNameFromToken(authToken); LOGGER.info("checking username:{}", username); // SecurityContextHolder 是 SpringSecurity 的一个工具类 // 保存应用程序中当前使用人的安全上下文 if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { // 根据用户名获取登录用户信息 UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); // 验证 token 是否过期 if (jwtTokenUtil.validateToken(authToken, userDetails)) { // 将登录用户保存到安全上下文中 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); LOGGER.info("authenticated user:{}", username); } } } chain.doFilter(request, response); } }
-
-
JWTtokenUtil
-
JWTtoken的工具类
-
/** * JwtToken生成的工具类 * JWT token的格式:header.payload.signature * header的格式(算法、token的类型): * {"alg": "HS512","typ": "JWT"} * payload的格式(用户名、创建时间、生成时间): * {"sub":"wang","created":1489079981393,"exp":1489684781} * signature的生成算法: * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) * on 2018/4/26. */ public class JwtTokenUtil { private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class); private static final String CLAIM_KEY_USERNAME = "sub"; private static final String CLAIM_KEY_CREATED = "created"; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; @Value("${jwt.tokenHead}") private String tokenHead; /** * 根据用户名、创建时间生成JWT的token */ private String generateToken(Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } /** * 从token中获取JWT中的负载 */ private Claims getClaimsFromToken(String token) { Claims claims = null; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { LOGGER.info("JWT格式验证失败:{}", token); } return claims; } /** * 生成token的过期时间 */ private Date generateExpirationDate() { return new Date(System.currentTimeMillis() + expiration * 1000); } /** * 从token中获取登录用户名 */ public String getUserNameFromToken(String token) { String username = null; Claims claims = getClaimsFromToken(token); if (claims != null) { username = claims.getSubject(); } return username; } /** * 验证token是否还有效 * * @param token 客户端传入的token * @param userDetails 从数据库中查询出来的用户信息 */ public boolean validateToken(String token, UserDetails userDetails) { String username = getUserNameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } /** * 判断token是否已经失效 */ private boolean isTokenExpired(String token) { Date expiredDate = getExpiredDateFromToken(token); return expiredDate.before(new Date()); } /** * 从token中获取过期时间 */ private Date getExpiredDateFromToken(String token) { Claims claims = getClaimsFromToken(token); return claims.getExpiration(); } /** * 根据用户信息生成token */ public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } /** * 当原来的token没过期时是可以刷新的 * * @param oldToken 带tokenHead的token */ public String refreshHeadToken(String oldToken) { if(StrUtil.isEmpty(oldToken)){ return null; } String token = oldToken.substring(tokenHead.length()); if(StrUtil.isEmpty(token)){ return null; } //token校验不通过 Claims claims = getClaimsFromToken(token); if(claims==null){ return null; } //如果token已经过期,不支持刷新 if(isTokenExpired(token)){ return null; } //如果token在30分钟之内刚刷新过,返回原token if(tokenRefreshJustBefore(token,30*60)){ return token; }else{ claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } } /** * 判断token在指定时间内是否刚刚刷新过 * @param token 原token * @param time 指定时间(秒) */ private boolean tokenRefreshJustBefore(String token, int time) { Claims claims = getClaimsFromToken(token); Date created = claims.get(CLAIM_KEY_CREATED, Date.class); Date refreshDate = new Date(); //刷新时间在创建时间的指定时间内 if(refreshDate.after(created)&&refreshDate.before(DateUtil.offsetSecond(created,time))){ return true; } return false; } }
-
-
RestAuthenticationEntryPoint
-
自定义返回结果,未登录或者过期
-
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Cache-Control","no-cache"); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); response.getWriter().println(JSONUtil.parse(ResultObject.unauthorized(authException.getMessage()))); response.getWriter().flush(); } }
-
-
RestfulAccessDeniedHandler
-
自定义返回结果,没有权限访问时
-
public class RestfulAccessDeniedHandler implements AccessDeniedHandler{ @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Cache-Control","no-cache"); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); response.getWriter().println(JSONUtil.parse(ResultObject.forbidden(e.getMessage()))); response.getWriter().flush(); } }
-
-
IgnoreUrlsConfig
-
配置不需要安全保护的资源
-
@Getter @Setter @ConfigurationProperties(prefix = "secure.ignored") public class IgnoreUrlsConfig { private List<String> urls = new ArrayList<>(); }
-
-
SecurityConfig
-
SpringSecurity通用配置
-
public class CustomSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired(required = false) private DynamicSecurityService dynamicSecurityService; @Override protected void configure(HttpSecurity httpSecurity) throws Exception { ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity .authorizeRequests(); //允许跨域请求的OPTIONS请求 registry.antMatchers(HttpMethod.OPTIONS) .permitAll(); //不需要保护的资源路径允许访问 for (String url : ignoreUrlsConfig().getUrls()) { registry.antMatchers(url).permitAll(); } // 任何请求需要身份认证 registry.and() .authorizeRequests() .anyRequest() .authenticated() // 关闭跨站请求防护及不使用session .and() .csrf() .disable() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 自定义权限拒绝处理类 .and() .exceptionHandling() .accessDeniedHandler(restfulAccessDeniedHandler()) .authenticationEntryPoint(restAuthenticationEntryPoint()) // 自定义权限拦截器JWT过滤器 .and() .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class); //有动态权限配置时添加动态权限校验过滤器 if(dynamicSecurityService!=null){ registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class); } } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()) .passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() { return new JwtAuthenticationTokenFilter(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public RestfulAccessDeniedHandler restfulAccessDeniedHandler() { return new RestfulAccessDeniedHandler(); } @Bean public RestAuthenticationEntryPoint restAuthenticationEntryPoint() { return new RestAuthenticationEntryPoint(); } @Bean public IgnoreUrlsConfig ignoreUrlsConfig() { return new IgnoreUrlsConfig(); } @Bean public JwtTokenUtil jwtTokenUtil() { return new JwtTokenUtil(); } @ConditionalOnBean(name = "dynamicSecurityService") @Bean public DynamicAccessDecisionManager dynamicAccessDecisionManager() { return new DynamicAccessDecisionManager(); } @ConditionalOnBean(name = "dynamicSecurityService") @Bean public DynamicSecurityFilter dynamicSecurityFilter() { return new DynamicSecurityFilter(); } @ConditionalOnBean(name = "dynamicSecurityService") @Bean public DynamicSecurityMetadataSource dynamicSecurityMetadataSource() { return new DynamicSecurityMetadataSource(); } }
-
Codingmore动态权限配置
不同的角色拥有不同的权限,需要对他们被允许进行的操作进行区分。
数据库划分:
user role admin_role_relation
role resource role_resource_relation
用户与角色关联,角色对应一组允许操作的资源。
首先,配置路径与资源的对应关系。通过实现DynamicSecurityService中的loadDataSource()方法,将需要资源的路径与资源一一对应。该接口的实现类位于condingmore-admin的CodingmoreSecurityConfig中配置。
配置完成后,第一步需要一个动态权限过滤器,用于实现基于路径的动态权限过滤。
其中,OPTIONS请求放行,白名单请求放行,需要鉴权的请求调用AccessDecisionManager中的decide()方法进行鉴权操作。
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
//OPTIONS请求直接放行
if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
return;
}
//白名单请求直接放行
PathMatcher pathMatcher = new AntPathMatcher();
for (String path : ignoreUrlsConfig.getUrls()) {
if(pathMatcher.match(path,request.getRequestURI())){
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
return;
}
}
//此处会调用AccessDecisionManager中的decide方法进行鉴权操作
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
实现动态权限决策管理器,判断用户是否有访问权限。decide()方法中将接口需要的资源与用户拥有的资源进行对比,用户拥有权限直接返回,没有权限抛出AccessDeniedException异常。
decide()方法如下
@Override
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
// 当接口未被配置资源时直接放行
if (CollUtil.isEmpty(configAttributes)) {
return;
}
Iterator<ConfigAttribute> iterator = configAttributes.iterator();
while (iterator.hasNext()) {
ConfigAttribute configAttribute = iterator.next();
//将访问所需资源或用户拥有资源进行比对
String needAuthority = configAttribute.getAttribute();
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("抱歉,您没有访问权限");
}
在DynamicSecurityFilter中调用super.beforeInvocation(fi)方法时会调用AccessDecisionManager中的decide方法用于鉴权操作,而decide方法中的configAttributes参数会通过SecurityMetadataSource中的getAttributes方法来获取,configAttributes其实就是配置好的访问当前接口所需要的权限。
getAttributes()方法如下:
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
if (configAttributeMap == null) this.loadDataSource();
List<ConfigAttribute> configAttributes = new ArrayList<>();
//获取当前访问的路径
String url = ((FilterInvocation) o).getRequestUrl();
String path = URLUtil.getPath(url);
PathMatcher pathMatcher = new AntPathMatcher();
Iterator<String> iterator = configAttributeMap.keySet().iterator();
//获取访问该路径所需资源
while (iterator.hasNext()) {
String pattern = iterator.next();
if (pathMatcher.match(pattern, path)) {
configAttributes.add(configAttributeMap.get(pattern));
}
}
// 未设置操作请求权限,返回空集合
return configAttributes;
}
455

被折叠的 条评论
为什么被折叠?



