文档
Spring Boot Reference Documentation
前后端分离的项目,需要后端认证成功之后,颁发令牌 token
,下次客户端在请求资源的时候,需要携带 access_token
参数。
Spring Security 生成 token 的基本思路是:
successHandler
中根据 Authentication
生成 token,响应给前端TokenAuthenticationFilter
当前请求参数中有参数 access_token
,解析token,获取 Authentication
放入 SecurityContext 上下文中。<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
</dependency>
<dependency>
<groupId>com.rick.common</groupId>
<artifactId>sharp-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
TokenAuthenticationFilter.java
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String accessToken = request.getParameter("access_token");
if (StringUtils.isBlank(accessToken)) {
chain.doFilter(request, response);
return;
}
try {
Authentication authentication = JWTUtils.toAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} catch (Exception e) {
HttpServletResponseUtils.write(response, "application/json;charset=UTF-8"
, JsonUtils.toJson(ResultUtils.exception(403, e.getMessage())));
}
}
}
WebSecurityConfig.java
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled=true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests((requests) -> {
((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl) requests.anyRequest()).authenticated();
})
.formLogin()
// 1. 认证成功后将token响应给前端
.successHandler((request, response, authentication) -> {
String token = JWTUtils.createToken(authentication);
HttpServletResponseUtils.write(response, "application/json;charset=UTF-8"
, JsonUtils.toJson(ResultUtils.success(token)));
})
.and().exceptionHandling().authenticationEntryPoint((request, response, e) -> {
HttpServletResponseUtils.write(response, "application/json;charset=UTF-8"
, JsonUtils.toJson(ResultUtils.exception(403, e.getMessage())));
})
.and().exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
HttpServletResponseUtils.write(response, "application/json;charset=UTF-8"
, JsonUtils.toJson(ResultUtils.exception(403, e.getMessage())));
}
}).and().addFilterBefore(new TokenAuthenticationFilter(), BasicAuthenticationFilter.class)
.csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
TokenAuthenticationFilter
successHandler
中根据 Authentication
生成 token,响应给前端application.yml
spring:
security:
user:
name: rick
password: 123456
roles: ADMIN
IndexController.java
@RestController
public class IndexController {
@GetMapping
public String index() {
return "index";
}
@GetMapping("admin")
public String admin(Authentication authentication) {
return "admin ==> ";
}
@GetMapping("p1")
@PreAuthorize("hasRole('p1')")
public String p1(Authentication authentication) {
return authentication.getPrincipal().toString();
}
}
curl -X GET \
http://127.0.0.1:8080/admin
响应
{
"success": false,
"code": 403,
"msg": "Full authentication is required to access this resource"
}
curl -X POST \
'http://127.0.0.1:8080/login?username=rick&password=123456'
响应
{
"success": true,
"code": 0,
"msg": "OK",
"data": "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC94aG9wZS50b3AiLCJzdWIiOiJvcmcuc3ByaW5nZnJhbWV3b3JrLnNlY3VyaXR5LmNvcmUudXNlcmRldGFpbHMuVXNlciBbVXNlcm5hbWU9cmljaywgUGFzc3dvcmQ9W1BST1RFQ1RFRF0sIEVuYWJsZWQ9dHJ1ZSwgQWNjb3VudE5vbkV4cGlyZWQ9dHJ1ZSwgY3JlZGVudGlhbHNOb25FeHBpcmVkPXRydWUsIEFjY291bnROb25Mb2NrZWQ9dHJ1ZSwgR3JhbnRlZCBBdXRob3JpdGllcz1bUk9MRV9BRE1JTl1dIiwiZXhwIjoxNjM0MzYyMzIxLCJhdXRob3JpdGllcyI6WyJST0xFX0FETUlOIl19.e4-tJzixAkOk9xeoX9zkehYjaH_DmBPe9tho6cCtu6M"
}
curl -X GET \
'http://127.0.0.1:8080/admin?access_token=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC94aG9wZS50b3AiLCJzdWIiOiJvcmcuc3ByaW5nZnJhbWV3b3JrLnNlY3VyaXR5LmNvcmUudXNlcmRldGFpbHMuVXNlciBbVXNlcm5hbWU9cmljaywgUGFzc3dvcmQ9W1BST1RFQ1RFRF0sIEVuYWJsZWQ9dHJ1ZSwgQWNjb3VudE5vbkV4cGlyZWQ9dHJ1ZSwgY3JlZGVudGlhbHNOb25FeHBpcmVkPXRydWUsIEFjY291bnROb25Mb2NrZWQ9dHJ1ZSwgR3JhbnRlZCBBdXRob3JpdGllcz1bUk9MRV9BRE1JTl1dIiwiZXhwIjoxNjM0MzYyMzIxLCJhdXRob3JpdGllcyI6WyJST0xFX0FETUlOIl19.e4-tJzixAkOk9xeoX9zkehYjaH_DmBPe9tho6cCtu6M'
响应:token正确
admin ==>
能正常访问资源信息。
响应:如果token错误
{
"success": false,
"code": 403,
"msg": "无效的token"
}
/p
curl -X GET \
'http://127.0.0.1:8080/p1?access_token=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC94aG9wZS50b3AiLCJzdWIiOiJvcmcuc3ByaW5nZnJhbWV3b3JrLnNlY3VyaXR5LmNvcmUudXNlcmRldGFpbHMuVXNlciBbVXNlcm5hbWU9cmljaywgUGFzc3dvcmQ9W1BST1RFQ1RFRF0sIEVuYWJsZWQ9dHJ1ZSwgQWNjb3VudE5vbkV4cGlyZWQ9dHJ1ZSwgY3JlZGVudGlhbHNOb25FeHBpcmVkPXRydWUsIEFjY291bnROb25Mb2NrZWQ9dHJ1ZSwgR3JhbnRlZCBBdXRob3JpdGllcz1bUk9MRV9BRE1JTl1dIiwiZXhwIjoxNjM0MzYyMzIxLCJhdXRob3JpdGllcyI6WyJST0xFX0FETUlOIl19.e4-tJzixAkOk9xeoX9zkehYjaH_DmBPe9tho6cCtu6M'
响应
{
"success": false,
"code": 403,
"msg": "Access is denied"
}
源代码:https://github.com/jkxyx205/spring-security-learn/tree/master/spring-security-token
如果接入多种登录方式,如:平台用户名密码登录、手机验证码登录,第三方登录(微信、QQ)。可以在验证通过之后在 AuthenticationSuccessHandler
中 通过 Authentication
生成手动生成token。
TokenGenerator.java
@Component
public class TokenGenerator {
@Autowired
@Qualifier("tokenServices")
private AuthorizationServerTokenServices authorizationServerTokenServices;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private TokenStore tokenStore;
@Autowired
private StringRedisTemplate stringRedisTemplate;
private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
/**
* 表单验证完成后创建token
* @param authentication
* @return
*/
public OAuth2AccessToken createToken(Authentication authentication) {
ClientDetails clientDetails = clientDetailsService.loadClientByClientId("c");
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) authentication;
TokenRequest tokenRequest = tokenRequest();
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, usernamePasswordAuthenticationToken);
OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
return token;
}
/**
* 刷新token
* @param accessToken
* @return
*/
public OAuth2AccessToken refresh(String accessToken) {
OAuth2AccessToken _accessToken = tokenStore.readAccessToken(accessToken);
if (Objects.nonNull(_accessToken) && !_accessToken.isExpired()) {
return _accessToken;
}
String refreshToken = stringRedisTemplate.opsForValue().get(ACCESS_TO_REFRESH + accessToken);
return authorizationServerTokenServices.refreshAccessToken(refreshToken, tokenRequest());
}
private TokenRequest tokenRequest() {
ClientDetails clientDetails = clientDetailsService.loadClientByClientId("c");
TokenRequest tokenRequest = new TokenRequest(Collections.emptyMap(),
clientDetails.getClientId(),
clientDetails.getScope(),
"all");
return tokenRequest;
}
}
@SpringBootTest
public class TokenGeneratorTest {
@Autowired
private TokenGenerator tokenGenerator;
@Test
public void testCreateToken() {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken("rick", "123456"
, AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN, p1, p2"));
OAuth2AccessToken token = tokenGenerator.createToken(usernamePasswordAuthenticationToken);
System.out.println(token.getExpiration());
System.out.println(token);
}
@Test
public void testRefresh() {
String accessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzIl0sInVzZXJfbmFtZSI6InJpY2siLCJzY29wZSI6WyJhbGwiXSwiaGVsbG8iOiJ3b3JsZCIsImV4cCI6MTYzNDMwMjkyNCwiYXV0aG9yaXRpZXMiOlsicDEiLCJwMiIsIkFETUlOIl0sImp0aSI6Ik5HZ0hEYUZiZFdXeWEyTUN1ZDFGZ0N5T1gyRSIsImNsaWVudF9pZCI6ImMifQ.X0Y9jXHD4u9aszh9lXs0flnyuNBm3lJ2Swiy49TxSqw";
OAuth2AccessToken token = tokenGenerator.refresh(accessToken);
System.out.println(token.getValue().equals(accessToken));
}
}
authorization_code
需要进行登录页面验证,所以授权服务器和资源服务器需要分开部署!password
授权服务器和资源服务器可以部署在一起。pom.xml
<!-- spring security 4.2.15 -->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.5.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
AuthorizationServer.java
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
@Qualifier("tokenServices")
private AuthorizationServerTokenServices authorizationServerTokenServices;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.tokenKeyAccess("permitAll()") // oauth/token_key是公开
.checkTokenAccess("permitAll()") // oauth/check_token公开
.allowFormAuthenticationForClients(); //密码模式:表单认证(申请令牌)
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()// 使用in-memory存储
.withClient("c")// client_id
.secret("123456")//客户端密钥
.resourceIds("res")//资源列表
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
.scopes("all")// 允许的授权范围
.autoApprove(false)//false跳转到授权页面
//加上验证回调地址
.redirectUris("http://www.baidu.com");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.userDetailsService(userDetailsService)
.authenticationManager(authenticationManager)// 认证管理器 => 密码模式需要在认证服务器中设置 中配置AuthenticationManager
.tokenServices(authorizationServerTokenServices)
.authorizationCodeServices(authorizationCodeServices)
.allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET);
}
@Bean
public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder) {
List<AuthenticationProvider> providers = new ArrayList<>();
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService(passwordEncoder));
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
providers.add(daoAuthenticationProvider);
return new ProviderManager(providers);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
//Admin Role
UserDetails theUser = User.withUsername("rick")
.password(passwordEncoder.encode("123456"))
.roles("ADMIN", "p1").build();
//User Role
UserDetails theManager = User.withUsername("john")
.password(passwordEncoder.encode("123456"))
.roles("USER").build();
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(theUser);
userDetailsManager.createUser(theManager);
return userDetailsManager;
}
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}
TokenConfig.java
@Configuration
public class TokenConfig {
private String SIGNING_KEY = "uaa123";
@Bean
public TokenStore tokenStore() {
//使用内存存储令牌(普通令牌)
// return new InMemoryTokenStore();
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
return converter;
}
// 令牌管理服务
@Bean("tokenServices")
public AuthorizationServerTokenServices tokenService(ClientDetailsService clientDetailsService, TokenStore tokenStore) {
DefaultTokenServices service=new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService);//客户端详情服务
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore);//令牌存储策略
// 令牌增强,支持Jwt令牌
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
// 注意顺序accessTokenConverter在最后
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
service.setTokenEnhancer(tokenEnhancerChain);
service.setReuseRefreshToken(false); // 只对"存储"的有效,jwt_stoken无效
service.setAccessTokenValiditySeconds(60); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
@Bean
public TokenEnhancer tokenEnhancer() {
return (oAuth2AccessToken, oAuth2Authentication) -> {
Map<String, Object> info = new HashMap<>();
info.put("hello", "world");
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
return oAuth2AccessToken;
};
}
}
WebSecurityConfig
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
application.yml
server:
port: 8081
redis
中,需要以下两步pom.xml
依赖 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
修改 TokenStore
@Bean
public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) {
return new RedisTokenStore(redisConnectionFactory);
}
pom.xml
<!-- spring security 4.2.15 -->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.5.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
ResourceServer.java
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled=true, prePostEnabled = true)
public class ResourceServer extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
// 资源id
resources.resourceId("res")
.stateless(true)
.accessDeniedHandler((request, response, e) -> {
response.getWriter().write(e.getMessage());
});
}
@Override
public void configure(HttpSecurity http) throws Exception {
((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl)http.authorizeRequests()
.antMatchers("/**").access("#oauth2.hasScope('all')")
.antMatchers("/admin").hasRole("ADMIN")
.anyRequest()).authenticated();
}
}
DefaultTokenServices
。 配置TokenStore(必须和AuthorizationServer相同,才能解析)TokenConfig.java
@Configuration
public class TokenConfig {
private String SIGNING_KEY = "uaa123";
@Bean
public TokenStore tokenStore() {
//使用内存存储令牌(普通令牌)
// return new InMemoryTokenStore();
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
return converter;
}
}
RemoteTokenServices
(推荐)security:
oauth2:
client:
client-id: c
client-secret: 123456
access-token-uri: http://localhost:8081/oauth/token
user-authorization-uri: http://localhost:8081/oauth/authorize
resource:
token-info-uri: http://localhost:8081/oauth/check_token
IndexController.java
@RestController
public class IndexController {
@GetMapping
public String index() {
return "index";
}
@GetMapping("admin")
public String admin(Authentication authentication, @RequestParam("access_token") String accessToken, Jwt jwt) {
return jwt.getClaims();
}
@GetMapping("p1")
@PreAuthorize("hasRole('p1')")
public String p1(Authentication authentication, @RequestParam("access_token") String accessToken) {
Jwt jwt = JwtHelper.decode(accessToken);
return jwt.getClaims();
}
}
application.yml
server:
port: 8082
http://localhost:8081/oauth/authorize?response_type=code&client_id=c&redirect_uri=http://www.baidu.com&scope=all
跳转到登录页面:输入用户名密码:rick/123456;允许授权,获取地址栏授权码 code
。
code
获取tokencurl -X POST \
http://localhost:8081/oauth/token \
-H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
-F code=cP8TeB \
-F grant_type=authorization_code \
-F client_id=c \
-F client_secret=123456 \
-F redirect_uri=http://www.baidu.com \
-F scope=all
返回 access_token
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzIl0sInVzZXJfbmFtZSI6InJpY2siLCJzY29wZSI6WyJhbGwiXSwiaGVsbG8iOiJ3b3JsZCIsImV4cCI6MTYzNDI4NTg2MSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfcDEiXSwianRpIjoienFOUFZnQVpCeFdwaE9WM1hWZjNZazNkZklVIiwiY2xpZW50X2lkIjoiYyJ9.C7NQVGDgexoZ0VSpbfH_hw1eVXV7JBmB7EH9YydDQlg",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzIl0sInVzZXJfbmFtZSI6InJpY2siLCJzY29wZSI6WyJhbGwiXSwiYXRpIjoienFOUFZnQVpCeFdwaE9WM1hWZjNZazNkZklVIiwiaGVsbG8iOiJ3b3JsZCIsImV4cCI6MTYzNDU0NTAwMSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfcDEiXSwianRpIjoiRXdWOXMtYk9mdktGM1V0SFNUc2h3b285dGk0IiwiY2xpZW50X2lkIjoiYyJ9.AWTDhc8Sl4M4tTRC4AdJ1njyhQF5oAXTDNeIfAt3b1o",
"expires_in": 59,
"scope": "all",
"hello": "world",
"jti": "zqNPVgAZBxWphOV3XVf3Yk3dfIU"
}
curl -X POST \
'http://localhost:8081/oauth/check_token?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzIl0sInVzZXJfbmFtZSI6InJpY2siLCJzY29wZSI6WyJhbGwiXSwiaGVsbG8iOiJ3b3JsZCIsImV4cCI6MTYzNDI5MTkyNiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfcDEiXSwianRpIjoiRTgwajVrclhSNElHZmdvUmV1UXlrc29xQ2NvIiwiY2xpZW50X2lkIjoiYyJ9.xj_Ui_La2XjrUeSbufXunobiyzbevzPOppcknJig7d0'
access_token
获取资源curl -X GET \
'http://localhost:8082/p1?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzIl0sInVzZXJfbmFtZSI6InJpY2siLCJzY29wZSI6WyJhbGwiXSwiaGVsbG8iOiJ3b3JsZCIsImV4cCI6MTYzNDI4NjAyMSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfcDEiXSwianRpIjoibDVVcHF3UUlBWDBMRFVQYWg1T1Q1RDdrVFgwIiwiY2xpZW50X2lkIjoiYyJ9.pknFynXTl8koIUATnoShKbQSOZnwgmP8g4943lfHK7w'
TableGridService
依赖于工具类 GridUtils
,可以在 Controller
层,通过SQL构造对象,直接从 HttpServletRequest
获取请求参数。
@GetMapping("request")
public Grid request(HttpServletRequest request) {
return new DefaultTableGridService("SELECT id, title, work_time FROM t_demo WHERE title like :title")
.list(request);
}
也可以手动指定查询count的SQL,提高查询效率。
@GetMapping("request")
public Grid request(HttpServletRequest request) {
return new DefaultTableGridService("SELECT id, title, work_time FROM t_demo WHERE title like :title",
"SELECT count(*) FROM t_demo WHERE title like :title")
.list(request);
}