使用 Liferay 在 REST 服务中进行身份验证和授权
Authentication and authorization in REST Services with Liferay
我们正在构建一些将通过 RESTful API 公开的服务。 API 的主要客户是使用 Angular JS 的 Liferay portlet,这意味着 client-side (Angular) 可以直接调用我们的服务。
到目前为止,我们已经设计了一种身份验证和授权机制,以确保我们可以识别哪个登录用户 (Liferay) 正在请求我们的 API。
PS.: 请注意,虽然我们使用的是 Liferay,但它可以是任何其他基于 Java 的应用程序。
我们设计的是:
- 当用户登录我们的门户时,Liferay 会创建一个包含用户登录(或 ID)+ 客户端 IP + 时间戳的身份验证令牌。此令牌保存在 cookie 中;
- 在每次 REST 调用之前,Angular 读取此 cookie 并通过 HTTP header;
发送其内容
- 我们的服务 "decrypts" 发送 cookie 内容并验证时间戳是否有效,IP 是否相同,并且根据我们的业务规则,用户是否有权执行或阅读他想做的任何事情.
目前我们认为这种设计是一致的,并且根据我们选择创建此令牌的算法,我们相信这是一种安全的方法。
我们的疑惑是:
- 我们是否以某种方式重新发明轮子而不使用某种自定义提供程序的 HTTP 身份验证?怎么做?
- Spring 安全可以帮助我们吗?我们已经阅读了一些关于它的文章,但不清楚将它与 non-Spring 应用程序一起使用是否有意义;
- 这种方法是否存在我们没有考虑到的安全漏洞?
提前谢谢你。感谢您的帮助。
菲利普
查看 Single Sign On and Spring Security OAuth2 令牌身份验证。
示例如下:sso-with-oauth2-angular-js-and-spring-security.
请注意 Spring 4.2 可能有一些方便的 CORS support。
我无法用我目前的评分来提高某人的答案,但上面的答案可能是正确的方向。
听起来您需要调查的是名为 CORS 的东西,它通过跨站点脚本提供安全性。抱歉,我还不太清楚它是如何工作的(我也处于同样的情况),但这是 this NSA document on REST
的主题
Spring 安全解决了问题描述,作为奖励,您将免费获得所有 spring 安全功能。
令牌方法很棒,这里是您如何使用 spring-security 保护您的 API
实现 AuthenticationEntryPoint 并设置开始方法 401 而不是 re-direction 3XX,如下所示
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Access Denied");
- 让 TokenProcessingFilter 扩展并利用 UsernamePasswordAuthenticationFilter 必须提供的功能,覆盖 doFilter() 方法,从请求中提取令牌 headers,如下验证和验证令牌
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = this.getAsHttpRequest(request);
String authToken = this.extractAuthTokenFromRequest(httpRequest);
String userName = TokenUtils.getUserNameFromToken(authToken);
if (userName != null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userName);
if (TokenUtils.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
您的 Spring-security 配置将类似于
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthFailure authFailure;
@Autowired
private AuthSuccess authSuccess;
@Autowired
private EntryPointUnauthorizedHandler unauthorizedHandler;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationTokenProcessingFilter authTokenProcessingFilter;
@Autowired
public void configureAuthBuilder(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // Restful hence stateless
.and()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler) // Notice the entry point
.and()
.addFilter(authTokenProcessingFilter) // Notice the filter
.authorizeRequests()
.antMatchers("/resources/**", "/api/authenticate").permitAll()
//.antMatchers("/admin/**").hasRole("ADMIN")
//.antMatchers("/providers/**").hasRole("ADMIN")
.antMatchers("/persons").authenticated();
}
}
-- 最后你需要另一个端点进行身份验证和 token-generation
这是一个 spring MVC 示例
@Controller
@RequestMapping(value="/api")
public class TokenGenerator{
@Autowired
@Lazy
private AuthenticationManager authenticationManager;
@Autowired
private UtilityBean utilityBean;
@Autowired
private UserDetailsService userDetailsService;
@RequestMapping(value="/authenticate", method=RequestMethod.POST, consumes=MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<?> generateToken(@RequestBody EmefanaUser user){
ResponseEntity<?> response = null;
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserId(),user.getCredential());
try {
Authentication authentication = authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
/*
* Reload user as password of authentication principal will be null
* after authorization and password is needed for token generation
*/
UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUserId());
String token = TokenUtils.createToken(userDetails);
response = ResponseEntity.ok(new TokenResource(utilityBean.encodePropertyValue(token)));
} catch (AuthenticationException e) {
response = ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
return response;
}
}
1生成token,2.后续API-calls应该有token
是的 spring-security 可以做到这一点,而且您不必在身份验证、授权方面开辟新天地。
- 希望对您有所帮助
我迟到了,但这是我的两分钱。
Disclaimer: The previous answers are a possible way to tackle this.
The next insight is what I've learned while implementing RESTful APIs
in Liferay.
如果我对问题的理解正确,那么这里有两种情况。第一个是您需要创建一个 RESTful api 将由已登录的用户调用。这意味着 AJAX 调用可能会在客户端呈现的门户中执行。这里的主要问题是安全性,即如何保护您的 REST 调用。
首先,我认为在实施其他东西之前,应该尝试利用正在使用的任何框架。 Liferay 确实在后端使用 Spring,但他们已经实现了安全性。我建议使用委托 Servlet。
此 servlet 将执行任何自定义 class 并将其放入 Liferay 的身份验证路径中,这意味着您可以只使用 PortalUtil.getUser(request)
,如果它为 0 或 null,则用户未通过身份验证。
为了使用委托 servlet,您只需要在 web.xml 文件
中配置它
<servlet>
<servlet-name>My Servlet</servlet-name>
<servlet-class>com.liferay.portal.kernel.servlet.PortalDelegateServlet</servlet-class>
<init-param>
<param-name>servlet-class</param-name>
<param-value>com.samples.MyClass</param-value>
</init-param>
<init-param>
<param-name>sub-context</param-name>
<param-value>api</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
如您所见,我们正在实例化另一个 servlet。该 servlet 将由 PortalDelegateServlet 定义。 Delegate Servlet 将使用 sevlet-class
参数值上的任何 class。在 class 中,您可以使用 Liferay 的 Utils 检查 HttpServletRequest 对象中是否存在有效的用户名,如果存在,则用户可以继续使用。
现在,您访问它的方式是委托 Servlet 使用 sub-context
的值来知道您从 URL 引用哪个 class。因此,在此示例中,您将通过转到 https://my.portal/delegate/api
来访问 com.samples.MyClass
'delegate' 部分将始终存在,URL 的第二部分是我们在init-param
。请注意,您只能为子上下文定义一层 URI,即您不能将 /api/v2.0/ 设置为子上下文。
从那时起,您可以在您的 servlet class 上做任何您想做的事情,并根据需要处理 REST URI 的解析。
您还可以使用 spring 的 Dispatcher class 作为 Delegate Servlet 将调用的 class 并且只需设置一个 spring servlet,因此 url 注释映射。
It is important to know that this is only good for RESTful or Resource
serving, since the Delegate Servlet will not know how to handle
renderization of views.
您拥有的第二种情况是能够从任何外部应用程序调用此 RESTful API(不管它们有什么实现)。这是一个完全不同的野兽,我必须参考 的答案,使用 Spring 的身份验证令牌可能是执行此操作的好方法。
另一种方法是在您的 servlet class 中处理未经授权的用户,方法是将他们发送到登录页面或类似的东西。一旦他们成功登录,Liferay 的 Utils 应该通过请求识别经过身份验证的用户。如果您想在外部应用程序中执行此操作,那么您需要模拟一个基于表单的登录,并且在整个过程中只使用相同的 cookie jar。 虽然我没试过,但理论上应该可以。话又说回来,理论上,共产主义是行得通的。
希望这能帮助其他可怜的人。
我们正在构建一些将通过 RESTful API 公开的服务。 API 的主要客户是使用 Angular JS 的 Liferay portlet,这意味着 client-side (Angular) 可以直接调用我们的服务。
到目前为止,我们已经设计了一种身份验证和授权机制,以确保我们可以识别哪个登录用户 (Liferay) 正在请求我们的 API。
PS.: 请注意,虽然我们使用的是 Liferay,但它可以是任何其他基于 Java 的应用程序。
我们设计的是:
- 当用户登录我们的门户时,Liferay 会创建一个包含用户登录(或 ID)+ 客户端 IP + 时间戳的身份验证令牌。此令牌保存在 cookie 中;
- 在每次 REST 调用之前,Angular 读取此 cookie 并通过 HTTP header; 发送其内容
- 我们的服务 "decrypts" 发送 cookie 内容并验证时间戳是否有效,IP 是否相同,并且根据我们的业务规则,用户是否有权执行或阅读他想做的任何事情.
目前我们认为这种设计是一致的,并且根据我们选择创建此令牌的算法,我们相信这是一种安全的方法。
我们的疑惑是:
- 我们是否以某种方式重新发明轮子而不使用某种自定义提供程序的 HTTP 身份验证?怎么做?
- Spring 安全可以帮助我们吗?我们已经阅读了一些关于它的文章,但不清楚将它与 non-Spring 应用程序一起使用是否有意义;
- 这种方法是否存在我们没有考虑到的安全漏洞?
提前谢谢你。感谢您的帮助。
菲利普
查看 Single Sign On and Spring Security OAuth2 令牌身份验证。
示例如下:sso-with-oauth2-angular-js-and-spring-security.
请注意 Spring 4.2 可能有一些方便的 CORS support。
我无法用我目前的评分来提高某人的答案,但上面的答案可能是正确的方向。 听起来您需要调查的是名为 CORS 的东西,它通过跨站点脚本提供安全性。抱歉,我还不太清楚它是如何工作的(我也处于同样的情况),但这是 this NSA document on REST
的主题Spring 安全解决了问题描述,作为奖励,您将免费获得所有 spring 安全功能。
令牌方法很棒,这里是您如何使用 spring-security 保护您的 API 实现 AuthenticationEntryPoint 并设置开始方法 401 而不是 re-direction 3XX,如下所示
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Access Denied");
- 让 TokenProcessingFilter 扩展并利用 UsernamePasswordAuthenticationFilter 必须提供的功能,覆盖 doFilter() 方法,从请求中提取令牌 headers,如下验证和验证令牌
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = this.getAsHttpRequest(request);
String authToken = this.extractAuthTokenFromRequest(httpRequest);
String userName = TokenUtils.getUserNameFromToken(authToken);
if (userName != null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userName);
if (TokenUtils.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
您的 Spring-security 配置将类似于
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthFailure authFailure;
@Autowired
private AuthSuccess authSuccess;
@Autowired
private EntryPointUnauthorizedHandler unauthorizedHandler;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationTokenProcessingFilter authTokenProcessingFilter;
@Autowired
public void configureAuthBuilder(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // Restful hence stateless
.and()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler) // Notice the entry point
.and()
.addFilter(authTokenProcessingFilter) // Notice the filter
.authorizeRequests()
.antMatchers("/resources/**", "/api/authenticate").permitAll()
//.antMatchers("/admin/**").hasRole("ADMIN")
//.antMatchers("/providers/**").hasRole("ADMIN")
.antMatchers("/persons").authenticated();
}
}
-- 最后你需要另一个端点进行身份验证和 token-generation 这是一个 spring MVC 示例
@Controller
@RequestMapping(value="/api")
public class TokenGenerator{
@Autowired
@Lazy
private AuthenticationManager authenticationManager;
@Autowired
private UtilityBean utilityBean;
@Autowired
private UserDetailsService userDetailsService;
@RequestMapping(value="/authenticate", method=RequestMethod.POST, consumes=MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<?> generateToken(@RequestBody EmefanaUser user){
ResponseEntity<?> response = null;
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserId(),user.getCredential());
try {
Authentication authentication = authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
/*
* Reload user as password of authentication principal will be null
* after authorization and password is needed for token generation
*/
UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUserId());
String token = TokenUtils.createToken(userDetails);
response = ResponseEntity.ok(new TokenResource(utilityBean.encodePropertyValue(token)));
} catch (AuthenticationException e) {
response = ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
return response;
}
}
1生成token,2.后续API-calls应该有token 是的 spring-security 可以做到这一点,而且您不必在身份验证、授权方面开辟新天地。
- 希望对您有所帮助
我迟到了,但这是我的两分钱。
Disclaimer: The previous answers are a possible way to tackle this. The next insight is what I've learned while implementing RESTful APIs in Liferay.
如果我对问题的理解正确,那么这里有两种情况。第一个是您需要创建一个 RESTful api 将由已登录的用户调用。这意味着 AJAX 调用可能会在客户端呈现的门户中执行。这里的主要问题是安全性,即如何保护您的 REST 调用。
首先,我认为在实施其他东西之前,应该尝试利用正在使用的任何框架。 Liferay 确实在后端使用 Spring,但他们已经实现了安全性。我建议使用委托 Servlet。
此 servlet 将执行任何自定义 class 并将其放入 Liferay 的身份验证路径中,这意味着您可以只使用 PortalUtil.getUser(request)
,如果它为 0 或 null,则用户未通过身份验证。
为了使用委托 servlet,您只需要在 web.xml 文件
<servlet>
<servlet-name>My Servlet</servlet-name>
<servlet-class>com.liferay.portal.kernel.servlet.PortalDelegateServlet</servlet-class>
<init-param>
<param-name>servlet-class</param-name>
<param-value>com.samples.MyClass</param-value>
</init-param>
<init-param>
<param-name>sub-context</param-name>
<param-value>api</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
如您所见,我们正在实例化另一个 servlet。该 servlet 将由 PortalDelegateServlet 定义。 Delegate Servlet 将使用 sevlet-class
参数值上的任何 class。在 class 中,您可以使用 Liferay 的 Utils 检查 HttpServletRequest 对象中是否存在有效的用户名,如果存在,则用户可以继续使用。
现在,您访问它的方式是委托 Servlet 使用 sub-context
的值来知道您从 URL 引用哪个 class。因此,在此示例中,您将通过转到 https://my.portal/delegate/api
来访问 com.samples.MyClass
'delegate' 部分将始终存在,URL 的第二部分是我们在init-param
。请注意,您只能为子上下文定义一层 URI,即您不能将 /api/v2.0/ 设置为子上下文。
从那时起,您可以在您的 servlet class 上做任何您想做的事情,并根据需要处理 REST URI 的解析。
您还可以使用 spring 的 Dispatcher class 作为 Delegate Servlet 将调用的 class 并且只需设置一个 spring servlet,因此 url 注释映射。
It is important to know that this is only good for RESTful or Resource serving, since the Delegate Servlet will not know how to handle renderization of views.
您拥有的第二种情况是能够从任何外部应用程序调用此 RESTful API(不管它们有什么实现)。这是一个完全不同的野兽,我必须参考
另一种方法是在您的 servlet class 中处理未经授权的用户,方法是将他们发送到登录页面或类似的东西。一旦他们成功登录,Liferay 的 Utils 应该通过请求识别经过身份验证的用户。如果您想在外部应用程序中执行此操作,那么您需要模拟一个基于表单的登录,并且在整个过程中只使用相同的 cookie jar。 虽然我没试过,但理论上应该可以。话又说回来,理论上,共产主义是行得通的。
希望这能帮助其他可怜的人。