Build RESTful APIs with Spring MVC:Security

#Secures APIsgit

We have configured Spring Security in before posts.angularjs

In this post, I will show you using Spring Security to protect APIs, aka provides Anthentication and Anthorization service for this sample application.github

  • Authentication answers the question: if the user is a valid user.
  • Authorization resolves the problem: if the authenticated user has corresponding permissions to access resources.

##Authenticationweb

In Spring security, it is easy to configure JAAS compatible authentication strategy, such as FORM, BASIC, X509 Certiciate etc.spring

Unlike JAAS in which the authentication management is very dependent on the container itself. Spring Security provides some extension points(such as UserDetails, UserDetailsService, Authority) and allows developers to customize and implement the authentication and authorization in a progammatic approach.api

Motioned in before posts, the simplest way to configure Spring security is using AuthenticationManagerBuilder to build essential required resources.spring-mvc

@Override
protected void configure(AuthenticationManagerBuilder auth)
		throws Exception {
	auth.inMemoryAuthentication()
			.passwordEncoder(passwordEncoder())
			.withUser("admin").password("test123").authorities("ROLE_ADMIN")
			.and()
				.withUser("test").password("test123").authorities("ROLE_USER");
}

An in-memory database and a HTTP BASIC authentication is easy to prototype applications, as showing as above codes.restful

If you want to store users into your database, firstly create a custom UserDetailsService bean and implement the findByUsername method and return a UserDetails object.mvc

public class SimpleUserDetailsServiceImpl implements UserDetailsService {

	private static final Logger log = LoggerFactory.getLogger(SimpleUserDetailsServiceImpl.class);

	private UserRepository userRepository;

	public SimpleUserDetailsServiceImpl(UserRepository userRepository) {
		this.userRepository = userRepository;
	}

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = userRepository.findByUsername(username);
		if (user == null) {
			throw new UsernameNotFoundException("username not found:" + username);
		}

		log.debug("found by username @" + username);

		return user;

	}

}

User class implements UserDetails.app

[@Data](http://my.oschina.net/difrik)
[@Builder](http://my.oschina.net/u/1245189)
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User implements UserDetails, Serializable {

	/**
	 *
	 */
	private static final long serialVersionUID = 1L;

	@Id()
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "id")
	private Long id;

	@Column(name = "username")
	private String username;

	@Column(name = "password")
	private String password;

	@Column(name = "name")
	private String name;

	@Column(name = "email")
	private String email;

	@Column(name = "role")
	private String role;

	@Column(name = "created_date")
	@CreatedDate
	private LocalDateTime createdDate;

	public String getName() {
		if (this.name == null || this.name.trim().length() == 0) {
			return this.username;
		}
		return name;
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return Arrays.asList(new SimpleGrantedAuthority("ROLE_" + this.role));
	}

	@Override
	public String getPassword() {
		return this.password;
	}

	@Override
	public String getUsername() {
		return this.username;
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public boolean isEnabled() {
		return true;
	}

}

Then configure AuthenticationManager with custom UserDetailsService instead of inMemoryAuthentication.

@Override
protected void configure(AuthenticationManagerBuilder auth)
		throws Exception {
	auth.userDetailsService(new SimpleUserDetailsServiceImpl(userRepository))
		.passwordEncoder(passwordEncoder);
}

If you want to design a customized Authentication strategy, you could have to create a custom AuthenticationEntryPoint and AuthenticationProvider for it. We will discuss this later.

##Anthorization

Once user is authenticated, when he tries to access some resources, such as URL, or execute some methods, it should check if the resource is protected, or has granted permissions on executing the methods.

Declarative URL pattern based authorizations

For REST APIs, the API resources are identified by URI, it is easy to grant authorizations via URL.

Override the configure(HttpSecurity) of WebSecurityConfigurerAdapter.

public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http   
			.authorizeRequests()   
			.antMatchers("/api/ping")
			.permitAll()
		.and()
			.authorizeRequests()   
			.antMatchers("/api/**")
			.authenticated()
			//....
	}

The access control is filter by the Matcher, there are two built-in matchers, Apache Ant path matcher, and perl like regex matchers. The later is a little complex, but more powerful.

http...antMatchers("/api/**").authenticated() means all resource URLs match '/api/**' need a valid authentication.

http...antMatchers(HttpMethod.POST, "/api/posts").hasRoles("ADMIN") indicates only users that have been granted ADMIN role have permission to create a new post.

Combined with resource URLs and HTTP methods, it follows rest convention exactly.

In the a real world application, you can centralize the URL pattern, HTTP Method, and granted ROLES into a certain persistent storage(such as RDBMS or NOSQL) and desgin a friendly web UI to control resource access.

Method level authorizations

Like JAAS, Spring Security provides several annotations to authorize access on method level.

Firstly you should add @EnableGlobalMethodSecurity on @Configuration class to enable it.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

}

If prePostEnabled is true, the @PreAuthorized and @PostAuthorized can be used, it accept Spring EL string and evaluate the result.

For example, only ADMIN user can save post.

@PreAuthorized("hasRole('ADMIN')")
public void savePost(Post post){}

And only the post owner can update the post.

@PreAuthorized("#post.author.id==principal.id")
public void update(Post post){}

jsr250Enabled provides Java common anntotation compatibility, and allow you use JAAS annotations in Spring project.

securedEnabled enables the legacy @Secured annotation which does not accept Spring EL as property value.

@Secured("ROLE_USER")	
public void savePost(Post post){}

Programmatic authorizations

Spring provides APIs to fetch current principal info.

For example, get current Authetication from SecurityContextHolder.

Authentication auth = SecurityContextHolder.getContext().getAuthentication();

And you can also inject current authenticated Principal like this.

publc List<Post> getByCurrentUser(@AuthenticationPrincipal Principal principal){}

After got the security principal info, you can control the authorizations in codes.

##Source Code

Check out sample codes from my github account.

git clone https://github.com/hantsy/angularjs-springmvc-sample

Or the Spring Boot version:

git clone https://github.com/hantsy/angularjs-springmvc-sample-boot

Read the live version of thess posts from Gitbook:Building RESTful APIs with Spring MVC.

相關文章
相關標籤/搜索