diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/config/WebSecurityConfigurer.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/config/WebSecurityConfigurer.java index 837c086aa..a29849a51 100644 --- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/config/WebSecurityConfigurer.java +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/config/WebSecurityConfigurer.java @@ -19,15 +19,24 @@ package com.pig4cloud.pigx.auth.config; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.pig4cloud.pigx.common.security.mobile.MobileLoginSuccessHandler; +import com.pig4cloud.pigx.common.security.mobile.MobileSecurityConfigurer; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; /** * @author lengleng @@ -38,14 +47,27 @@ import org.springframework.security.crypto.password.PasswordEncoder; @Order(90) @Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { + @Autowired + private ObjectMapper objectMapper; + @Autowired + private ClientDetailsService clientDetailsService; + @Autowired + private UserDetailsService pigxUserDetailsServiceImpl; + @Lazy + @Autowired + private AuthorizationServerTokenServices defaultAuthorizationServerTokenServices; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() - .antMatchers("/actuator/**", "/oauth/removeToken").permitAll() + .antMatchers( + "/actuator/**", + "/oauth/remvoveToken", + "/mobile/**").permitAll() .anyRequest().authenticated() - .and().csrf().disable(); + .and().csrf().disable() + .apply(mobileSecurityConfigurer()); } @Bean @@ -54,6 +76,22 @@ public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { return super.authenticationManagerBean(); } + @Bean + public AuthenticationSuccessHandler mobileLoginSuccessHandler() { + return MobileLoginSuccessHandler.builder() + .objectMapper(objectMapper) + .clientDetailsService(clientDetailsService) + .passwordEncoder(passwordEncoder()) + .defaultAuthorizationServerTokenServices(defaultAuthorizationServerTokenServices).build(); + } + + @Bean + public MobileSecurityConfigurer mobileSecurityConfigurer() { + return new MobileSecurityConfigurer(mobileLoginSuccessHandler() + , pigxUserDetailsServiceImpl); + } + + /** * https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-storage-updated * Encoded password does not look like BCrypt diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/RevokeTokenEndpoint.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/RevokeTokenEndpoint.java index 80ddb71ed..8e6384ac7 100644 --- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/RevokeTokenEndpoint.java +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/RevokeTokenEndpoint.java @@ -54,6 +54,7 @@ public class RevokeTokenEndpoint { } tokenStore.removeAccessToken(accessToken); } + return new R<>(Boolean.TRUE); } } diff --git a/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/SecurityConstants.java b/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/SecurityConstants.java index e3120b138..7fd7c2906 100644 --- a/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/SecurityConstants.java +++ b/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/SecurityConstants.java @@ -74,4 +74,8 @@ public interface SecurityConstants { */ String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ?"; + /** + * 手机号登录URL + */ + String MOBILE_TOKEN_URL = "/mobile/token"; } diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/mobile/MobileAuthenticationFilter.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/mobile/MobileAuthenticationFilter.java new file mode 100644 index 000000000..ece485983 --- /dev/null +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/mobile/MobileAuthenticationFilter.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2018-2025, lengleng All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * Author: lengleng (wangiegie@gmail.com) + */ + +package com.pig4cloud.pigx.common.security.mobile; + +import com.pig4cloud.pigx.common.core.constant.SecurityConstants; +import lombok.Getter; +import lombok.Setter; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author lengleng + * @date 2018/1/9 + * 手机号登录验证filter + */ +public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter { + private static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile"; + @Getter + @Setter + private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY; + @Getter + @Setter + private boolean postOnly = true; + + public MobileAuthenticationFilter() { + super(new AntPathRequestMatcher(SecurityConstants.MOBILE_TOKEN_URL, "POST")); + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, + HttpServletResponse response) throws AuthenticationException { + if (postOnly && !request.getMethod().equals(HttpMethod.POST.name())) { + throw new AuthenticationServiceException( + "Authentication method not supported: " + request.getMethod()); + } + + String mobile = obtainMobile(request); + + if (mobile == null) { + mobile = ""; + } + + mobile = mobile.trim(); + + MobileAuthenticationToken mobileAuthenticationToken = new MobileAuthenticationToken(mobile); + + setDetails(request, mobileAuthenticationToken); + + return this.getAuthenticationManager().authenticate(mobileAuthenticationToken); + } + + private String obtainMobile(HttpServletRequest request) { + return request.getParameter(mobileParameter); + } + + private void setDetails(HttpServletRequest request, + MobileAuthenticationToken authRequest) { + authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); + } +} + diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/mobile/MobileAuthenticationProvider.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/mobile/MobileAuthenticationProvider.java new file mode 100644 index 000000000..03b01012f --- /dev/null +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/mobile/MobileAuthenticationProvider.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2018-2025, lengleng All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * Author: lengleng (wangiegie@gmail.com) + */ + +package com.pig4cloud.pigx.common.security.mobile; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; + +/** + * @author lengleng + * @date 2018/8/5 + * 手机号登录校验逻辑 + */ +public class MobileAuthenticationProvider implements AuthenticationProvider { + @Getter + @Setter + private UserDetailsService userDetailsService; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + MobileAuthenticationToken mobileAuthenticationToken = (MobileAuthenticationToken) authentication; + UserDetails userDetails = userDetailsService.loadUserByUsername("admin"); + MobileAuthenticationToken authenticationToken = new MobileAuthenticationToken(userDetails, userDetails.getAuthorities()); + authenticationToken.setDetails(mobileAuthenticationToken.getDetails()); + return authenticationToken; + } + + @Override + public boolean supports(Class authentication) { + return MobileAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/mobile/MobileAuthenticationToken.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/mobile/MobileAuthenticationToken.java new file mode 100644 index 000000000..f059cab92 --- /dev/null +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/mobile/MobileAuthenticationToken.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2018-2025, lengleng All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * Author: lengleng (wangiegie@gmail.com) + */ + +package com.pig4cloud.pigx.common.security.mobile; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.SpringSecurityCoreVersion; + +import java.util.Collection; + +/** + * @author lengleng + * @date 2018/1/9 + * 手机号登录令牌 + */ +public class MobileAuthenticationToken extends AbstractAuthenticationToken { + + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + private final Object principal; + + public MobileAuthenticationToken(String mobile) { + super(null); + this.principal = mobile; + setAuthenticated(false); + } + + public MobileAuthenticationToken(Object principal, + Collection authorities) { + super(authorities); + this.principal = principal; + super.setAuthenticated(true); + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + if (isAuthenticated) { + throw new IllegalArgumentException( + "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); + } + + super.setAuthenticated(false); + } + + @Override + public void eraseCredentials() { + super.eraseCredentials(); + } +} + diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/mobile/MobileLoginSuccessHandler.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/mobile/MobileLoginSuccessHandler.java new file mode 100644 index 000000000..0f8555dcd --- /dev/null +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/mobile/MobileLoginSuccessHandler.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2018-2025, lengleng All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * Author: lengleng (wangiegie@gmail.com) + */ +package com.pig4cloud.pigx.common.security.mobile; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.CharsetUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.pig4cloud.pigx.common.core.constant.CommonConstant; +import com.pig4cloud.pigx.common.security.util.AuthUtils; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.exceptions.InvalidClientException; +import org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException; +import org.springframework.security.oauth2.provider.*; +import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestValidator; +import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * @author lengleng + * @date 2018/8/5 + * 手机号登录成功,返回oauth token + */ +@Slf4j +@Builder +public class MobileLoginSuccessHandler implements AuthenticationSuccessHandler { + private static final String BASIC_ = "Basic "; + private ObjectMapper objectMapper; + private PasswordEncoder passwordEncoder; + private ClientDetailsService clientDetailsService; + private AuthorizationServerTokenServices defaultAuthorizationServerTokenServices; + + /** + * Called when a user has been successfully authenticated. + * 调用spring security oauth API 生成 oAuth2AccessToken + * + * @param request the request which caused the successful authentication + * @param response the response + * @param authentication the Authentication object which was created during + */ + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + String header = request.getHeader("Authorization"); + + if (header == null || !header.startsWith(BASIC_)) { + throw new UnapprovedClientAuthenticationException("请求头中client信息为空"); + } + + try { + String[] tokens = AuthUtils.extractAndDecodeHeader(header); + assert tokens.length == 2; + String clientId = tokens[0]; + + ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId); + + //校验secret + if (!passwordEncoder.matches(tokens[1], clientDetails.getClientSecret())) { + throw new InvalidClientException("Given client ID does not match authenticated client"); + + } + + TokenRequest tokenRequest = new TokenRequest(MapUtil.newHashMap(), clientId, clientDetails.getScope(), "mobile"); + + //校验scope + new DefaultOAuth2RequestValidator().validateScope(tokenRequest, clientDetails); + OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails); + OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication); + OAuth2AccessToken oAuth2AccessToken = defaultAuthorizationServerTokenServices.createAccessToken(oAuth2Authentication); + log.info("获取token 成功:{}", oAuth2AccessToken.getValue()); + + response.setCharacterEncoding(CharsetUtil.UTF_8); + response.setContentType(CommonConstant.CONTENT_TYPE); + PrintWriter printWriter = response.getWriter(); + printWriter.append(objectMapper.writeValueAsString(oAuth2AccessToken)); + } catch (IOException e) { + throw new BadCredentialsException( + "Failed to decode basic authentication token"); + } + } + + +} diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/mobile/MobileSecurityConfigurer.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/mobile/MobileSecurityConfigurer.java new file mode 100644 index 000000000..d0ea01f69 --- /dev/null +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/mobile/MobileSecurityConfigurer.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2018-2025, lengleng All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * Author: lengleng (wangiegie@gmail.com) + */ + +package com.pig4cloud.pigx.common.security.mobile; + +import lombok.AllArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.stereotype.Component; + +/** + * @author lengleng + * @date 2018/8/5 + * 手机号登录配置入口 + */ +@Component +@AllArgsConstructor +public class MobileSecurityConfigurer extends SecurityConfigurerAdapter { + private AuthenticationSuccessHandler mobileLoginSuccessHandler; + private UserDetailsService userDetailsService; + + @Override + public void configure(HttpSecurity http) { + MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter(); + mobileAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); + mobileAuthenticationFilter.setAuthenticationSuccessHandler(mobileLoginSuccessHandler); + + MobileAuthenticationProvider mobileAuthenticationProvider = new MobileAuthenticationProvider(); + mobileAuthenticationProvider.setUserDetailsService(userDetailsService); + http.authenticationProvider(mobileAuthenticationProvider) + .addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/util/AuthUtils.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/util/AuthUtils.java new file mode 100644 index 000000000..d143f4d78 --- /dev/null +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/util/AuthUtils.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2018-2025, lengleng All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * Author: lengleng (wangiegie@gmail.com) + */ + +package com.pig4cloud.pigx.common.security.util; + +import cn.hutool.core.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.codec.Base64; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * @author lengleng + * @date 2018/5/13 + * 认证授权相关工具类 + */ +@Slf4j +public class AuthUtils { + private static final String BASIC_ = "Basic "; + + /** + * 从header 请求中的clientId/clientsecect + * + * @param header header中的参数 + * @throws RuntimeException if the Basic header is not present or is not valid + * Base64 + */ + public static String[] extractAndDecodeHeader(String header) + throws IOException { + + byte[] base64Token = header.substring(6).getBytes("UTF-8"); + byte[] decoded; + try { + decoded = Base64.decode(base64Token); + } catch (IllegalArgumentException e) { + throw new RuntimeException( + "Failed to decode basic authentication token"); + } + + String token = new String(decoded, CharsetUtil.UTF_8); + + int delim = token.indexOf(":"); + + if (delim == -1) { + throw new RuntimeException("Invalid basic authentication token"); + } + return new String[]{token.substring(0, delim), token.substring(delim + 1)}; + } + + /** + * *从header 请求中的clientId/clientsecect + * + * @param request + * @return + * @throws IOException + */ + public static String[] extractAndDecodeHeader(HttpServletRequest request) + throws IOException { + String header = request.getHeader("Authorization"); + + if (header == null || !header.startsWith(BASIC_)) { + throw new RuntimeException("请求头中client信息为空"); + } + + return extractAndDecodeHeader(header); + } +} diff --git a/pigx-config/src/main/resources/config/application-dev.yml b/pigx-config/src/main/resources/config/application-dev.yml index f6b0645e2..01bf8a6cb 100644 --- a/pigx-config/src/main/resources/config/application-dev.yml +++ b/pigx-config/src/main/resources/config/application-dev.yml @@ -88,10 +88,12 @@ swagger: - scope: 'server' description: 'server all' token-url-list: - - ${security.oauth2.resource.token-info-uri} + - ${security.auth.server}/token ## spring security 配置 security: + auth: + server: http://localhost:9999/auth/oauth oauth2: resource: - token-info-uri: http://localhost:9999/auth/oauth/check_token + token-info-uri: ${security.auth.server}/check_token diff --git a/pigx-visual/pigx-codegen/src/main/resources/mapper/SysGeneratorMapper.xml b/pigx-visual/pigx-codegen/src/main/resources/mapper/SysGeneratorMapper.xml index a60555241..256b917b9 100755 --- a/pigx-visual/pigx-codegen/src/main/resources/mapper/SysGeneratorMapper.xml +++ b/pigx-visual/pigx-codegen/src/main/resources/mapper/SysGeneratorMapper.xml @@ -9,16 +9,6 @@ and table_name like concat('%', #{tableName}, '%') order by create_time desc - - limit #{offset}, #{limit} - - - -