如何用 Spring 引导和 Spring 安全保护 REST API?

我知道保护 REST API 是一个被广泛评论的话题,但是我不能创建一个满足我的标准的小原型(我需要确认这些标准是现实的)。如何保护资源以及如何使用 Spring 安全性有很多选项,我需要澄清我的需求是否现实。

我的要求

  • 基于令牌的身份验证器用户将提供其凭证,并获得唯一和有时间限制的访问令牌。我希望在我自己的实现中管理令牌的创建、检查有效性和过期。
  • 一些 REST 资源将是公共的——根本不需要进行身份验证,
  • 有些资源只有具有管理员权限的用户才能访问,
  • 其他资源经过授权后可供所有用户访问。
  • 我不想使用基本身份验证
  • Java 代码配置(不是 XML)

现状

我的 REST API 工作得非常好,但是现在我需要保护它。当我在寻找一个解决方案时,我创建了一个 javax.servlet.Filter过滤器:

  @Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {


HttpServletRequest request = (HttpServletRequest) req;


String accessToken = request.getHeader(AUTHORIZATION_TOKEN);
Account account = accountDao.find(accessToken);


if (account == null) {
throw new UnauthorizedException();
}


chain.doFilter(req, res);


}

但是使用 javax.servlet.filters的这个解决方案不能按照我的需要工作,因为通过使用 Springservlet dispatcher@ControllerAdvice进行异常处理存在问题。

我需要的

我想知道这些标准是否切合实际,是否有帮助,如何开始使用 Spring Security 保护 REST API。我读了很多教程(例如 Spring Data REST + Spring 安全) ,但是所有的都是在非常基本的配置中工作的——使用 它们的凭证存储在内存中进行配置的用户,我需要使用 DBMS 并创建自己的身份验证器。

请给我一些如何开始的建议。

112809 次浏览

Token based authentication - users will provide its credentials and get unique and time limited access token. I would like to manage token creation, checking validity, expiration in my own implementation.

Actually, use Filter for token Auth - best way in this case

Eventually, you can create CRUD via Spring Data for managing Token's properties like to expire, etc.

Here is my token filter: http://pastebin.com/13WWpLq2

And Token Service Implementation

http://pastebin.com/dUYM555E

Some REST resources will be public - no need to authenticate at all

It's not a problem, you can manage your resources via Spring security config like this: .antMatchers("/rest/blabla/**").permitAll()

Some resources will be accessible only for users with administrator rights,

Take a look at @Secured annotation to class. Example:

@Controller
@RequestMapping(value = "/adminservice")
@Secured("ROLE_ADMIN")
public class AdminServiceController {

The other resource will be accessible after authorization for all users.

Back to Spring Security configure, you can configure your url like this:

    http
.authorizeRequests()
.antMatchers("/openforall/**").permitAll()
.antMatchers("/alsoopen/**").permitAll()
.anyRequest().authenticated()

I don't want to use Basic authentication

Yep, via token filter, your users will be authenticated.

Java code configuration (not XML)

Back to the words above, look at @EnableWebSecurity. Your class will be:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {}

You have to override the configure method. Code below, just for example, how to configure matchers. It's from another project.

    @Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/assets/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.usernameParameter("j_username")
.passwordParameter("j_password")
.loginPage("/login")
.defaultSuccessUrl("/", true)
.successHandler(customAuthenticationSuccessHandler)
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.invalidateHttpSession(true)
.logoutSuccessUrl("/")
.deleteCookies("JSESSIONID")
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.and()
.csrf();
}

I searched this long time too.I am working on a similar project.I found out Spring has a module to implement session via redis. It looks easy and useful. I will add to my project too. Can be helpful:

http://docs.spring.io/spring-session/docs/1.2.1.BUILD-SNAPSHOT/reference/html5/guides/rest.html

Spring security also very useful for providing authentication and authorization to the REST URLs. We no need to specify any custom implementations.

First, you need to specify the entry-point-ref to restAuthenticationEntryPoint in your security configuration as below.

 <security:http pattern="/api/**" entry-point-ref="restAuthenticationEntryPoint" use-expressions="true" auto-config="true" create-session="stateless" >


<security:intercept-url pattern="/api/userList" access="hasRole('ROLE_USER')"/>
<security:intercept-url pattern="/api/managerList" access="hasRole('ROLE_ADMIN')"/>
<security:custom-filter ref="preAuthFilter" position="PRE_AUTH_FILTER"/>
</security:http>

Implementation for the restAuthenticationEntryPoint might be as below.

 @Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {


public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException ) throws IOException {
response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized" );
}
}

After this you need to specify RequestHeaderAuthenticationFilter. It contains the RequestHeader key. This is basically used for identifying the user`s authentication. Generally RequestHeader carries this information while making the REST calls. For example consider below code

   <bean id="preAuthFilter" class="org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter">
<property name="principalRequestHeader" value="Authorization"/>
<property name="authenticationManager" ref="authenticationManager" />
</bean>

Here,

<property name="principalRequestHeader" value="Authorization"/>

"Authorization" is the the key presented the incoming request. It holds the required user`s authentication information. Also you need to configure the PreAuthenticatedAuthenticationProvider to fulfill our requirement.

   <bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<property name="preAuthenticatedUserDetailsService">
<bean id="userDetailsServiceWrapper"
class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
<property name="userDetailsService" ref="authenticationService"/>
</bean>
</property>
</bean>

This code will work for securing the REST urls by means of Authentication and authorization without any custom implementations.

For Complete code please find the below link:

https://github.com/srinivas1918/spring-rest-security

To validate REST API there are 2 ways

1 - Basic authentication using default username and password set up in application.properties file

Basic Authentication

2 - Authenticate using database (userDetailsService) with the actual username and password

Advanced Authentication

Another way that uses http.addFilterBefore() with custom filters

This solution is more like a skeleton to help you set up the basics. I've created a working demo and added some necessary comments to help understand the process. It comes with with some simple role-based and permission-based authentication/authorization, a publically accessable endpoint settings that you can easily pick up and use.

So it's better to see the full code, and run the app in action: github repo

User class set up:

public class User implements UserDetails {


private final String username;
private final String password;
private final List<? extends GrantedAuthority> grantedAuthorities;


public User(
String username,
String password,
List<? extends GrantedAuthority> grantedAuthorities
) {
this.username = username;
this.password = password;
this.grantedAuthorities = grantedAuthorities;
}


// And other default method overrides
}

Adding custom filters through addFilterBefore() method:

http
.authorizeRequests()
.antMatchers("/")
.permitAll()
.addFilterBefore( // Filter login request only
new LoginFilter("login", authenticationManager()),
UsernamePasswordAuthenticationFilter.class
)
.addFilterBefore( // Filter logout request only
new LogoutFilter("logout"),
UsernamePasswordAuthenticationFilter.class
)
.addFilterBefore( // Verify user on every request
new AuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class
);

Custom LoginFilter extends AbstractAuthenticationProcessingFilter, and override three methods, to deal with autentication:

public class LoginFilter extends AbstractAuthenticationProcessingFilter {


public LoginFilter(String url, AuthenticationManager authManager) {
super(url, authManager);
}


@Override
public Authentication attemptAuthentication(
HttpServletRequest req,
HttpServletResponse res
)
throws AuthenticationException, IOException {
LoginUserDto loginUserDto = new ObjectMapper() // this dto is a simple {username, password} object
.readValue(req.getInputStream(), LoginUserDto.class);


return getAuthenticationManager()
.authenticate(
new UsernamePasswordAuthenticationToken(
loginUserDto.getUsername(),
loginUserDto.getPassword()
)
);
}


@Override
protected void successfulAuthentication(
HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth
)
throws IOException, ServletException {
User user = (User) auth.getPrincipal();


req.getSession().setAttribute(UserSessionKey, user); // Simply put it in session


res.getOutputStream().print("You are logged in as " + user.getUsername());
}


@Override
protected void unsuccessfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException failed
)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("text/plain");
response.getOutputStream().print(failed.getMessage());
}
}

Custom AuthenticationFilter check for auth info stored in session and pass to SecurityContext:

public class AuthenticationFilter extends GenericFilterBean {


@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain filterChain
)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpSession session = req.getSession();


User user = (User) session.getAttribute(UserSessionKey);


if (user != null) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
user,
user.getPassword(),
user.getAuthorities()
);


SecurityContextHolder.getContext().setAuthentication(authToken);
}


// Either securityContext has authToken or not, we continue the filter chain
filterChain.doFilter(request, response);
}
}

Custom LogoutFilter is rather simple and straightforward, invalidate session and terminate authentication process:

public class LogoutFilter extends AbstractAuthenticationProcessingFilter {


public LogoutFilter(String url) {
super(url);
}


@Override
public Authentication attemptAuthentication(
HttpServletRequest req,
HttpServletResponse res
)
throws AuthenticationException, IOException {
req.getSession().invalidate();
res.getWriter().println("You logged out!");


return null;
}
}

A bit of explanation:

What these three custom filters do is that, login and logout filter only listen to their repective endpoint.

In login filter, we get username and password sent from client and check it against a DB(in real world) for validation, if it's valid user, then put it in session and pass it onto SecurityContext.

In logout filter, we simply invalidate the session and return a string.

While the custom AuthenticationFilter will authenticate every incomming request in an attempt to get user info from session, and then pass it onto SecurityContext.