This article need you already understand how to config Spring mvc in java-based configuration without Spring security. If not, check this article first on "Java-based Spring mvc configuration - without Spring security"
This article is a hello world level spring mvc application with following features:
- All configuration are java-based
- Spring security enabled
- User/password/rolename are stored in database. (MySQL, in this demo)
- Tables used to store username/password/rolename are not spring security's default schema. ( spring security has a default table schema for user and authories storage, but not flexiable enough).
This article can also be used as a hello world level example on how to use spring security in Spring webmvc application.
0. What you need
- Maven 3.2+
- Spring 4 +
- Spring security 4.+
1.Brief about using spring security
To use Spring security in your project you need to define your own class, MyUserDetailsService in this demo, which implements interface UserDetailsService(only-read user info) or interface UserDetailsManager(can create new user). These 2 interfaces are defined in Spring security. Spring security also provides a in-memory implements for above interfaces, but it's only for prototype or testing, not for real usage.
The user information type used in Spring security is a interface called UserDetails. To make your code clean, you can implements this interface on your user entity definition, so the entity loaded from database can be used in spring security framework directly.
Spring security internally use servlet filter to fulfill all the functions. You need to create a DaoAuthenticationProvider, and set your own MyUserDetailsService to it.
2. Cheet sheet
step1. Define entity class, implements spring security's interfaces (UserDetails and GrantedAuthority for user and authority respectively)
step2. Define class implements UserDetailsService or UserDetailsManager. In this demo it's MyUserDetailsServices.java
step3. Define a java config class extends WebSecurityConfigurerAdapter. In this demo it's SecurityConfig.java. There are 3 thnings defined in this class.
- A bean of type DaoAuthenticationProvider, will use UserDetailsService implementation defined in step 2.
- A config method which will use the just defined DaoAuthenticationProvider bean. ( A config method is a method annotated with @Autowired, method name is not important, arguments for the method will autowired from the spring context)
- Override configure(HttpSecurity http) method , set all access rules based on url patterns.
step4. Define a java class extends AbstractSecurityWebApplicationInitializer. In this demo it's SecurityWebApplicationInitializer.java.
step5. Finally make sure SecurityCinfig from step 3 is loaded. In this demo just add SecurityConfig to getRootConfigClasses() for class extends AbstractAnnotationConfigDispatcherServletInitializer.
The purple types are from Spring.
Still not clear on how to do? Don't worry, check the demo code below first, then come back to see whether you'v got it.
3. Demo
3.1. Define Entity class
There are 2 entity classes. One for username+password, the other one for user's rolename. One user can have more than one roles.
package com.shengwang.demo.model; import org.springframework.security.core.userdetails.UserDetails; // omit other import @Entity @Table(name="t_user") public class UserEntity implements UserDetails{ @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long userId; private String username; private String password; private Boolean enabled; // Make sense to Eager fetch user's authorities @OneToMany(mappedBy="user",fetch=FetchType.EAGER) private List<AuthorityEntity> authorities; // omit getter/setter // methods from UserDetails interface @Override public boolean isAccountNonExpired() { return true; // database has no mapping fields, always true } @Override public boolean isAccountNonLocked() { return true; // database has no mapping fields, always true } @Override public boolean isCredentialsNonExpired() { return true; // database has no mapping fields, always true } @Override public boolean isEnabled() { return enabled; } }
This UserEntity implements spring security's UserDetails interface. For simplicity, it only has 3 non-trivial fields, username, password and enabled besides the primary key. Another entity is defined in AuthorityEntity.java
package com.shengwang.demo.model; import org.springframework.security.core.GrantedAuthority; // omit other import @Entity @Table(name="t_authority") public class AuthorityEntity implements GrantedAuthority{ private static final long serialVersionUID = 1204090309640166925L; @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private long id; private String roleName; //bi-directional many-to-one association to User @ManyToOne @JoinColumn(name="userId") private UserEntity user; // omit getter/setter // mothod from interface GrantedAuthority @Override public String getAuthority() { return this.roleName; } }
The AuthorityEntity implments Spring security's GrantedAuthority interface. The only non-trivial field is string for role name. Here the points for entity definition are interfaces they implement.
3.2 Define class implements UserDetailsService
Here is our MyUserDetailsService. Choose a better name in your real project. It only has 1 method which access database, find user by username.
package com.shengwang.demo.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import com.shengwang.demo.model.UserRepository; @Service public class MyUserDetailsService implements UserDetailsService { @Autowired UserRepository userRepo; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return userRepo.findByUsername(username); } }
The mothod loadUserByUsername() access database to load the user entity. Since the user entity class implements the UserDetails interface, it can be returned directly.
3.3 Define config class extends from WebSecurityConfigurerAdapter
This SecurityConfig class, extends from WebSecurityConfigurerAdapter is the key of Spring security configuration. In fact it will finally cause Spring create "springSecuritFilterChain" eventually. This class has annotation @EnableWebSecurity.
package com.shengwang.demo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import com.shengwang.demo.service.MyUserDetailsService; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Logger logger = LoggerFactory.getLogger(SecurityConfig.class); // // In memory authentication, only for prototype or demo // @Autowired // public void configAuthenticationProvider(AuthenticationManagerBuilder auth) throws Exception { // logger.info("hello"); // auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); // } /** * 1. Define a DaoAuthenticationProvider bean. This bean use our own * UserDetailsService implementation for load user from database */ @Bean DaoAuthenticationProvider daoAuthenticationProvider(MyUserDetailsService myUserDetailsService) { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(myUserDetailsService); return daoAuthenticationProvider; } /** * 2. Use above bean for Authentication */ @Autowired public void configAuthenticationProvider(AuthenticationManagerBuilder auth, DaoAuthenticationProvider daoAuthenticationProvider) throws Exception { auth.authenticationProvider(daoAuthenticationProvider); } /** * 3. Define access rules based on request url pattern. * The rulese in this demo: * Anyone can access homepage / * Anyone can access static resources like /imgs/** * Only "ADMIN" role can access url under /admin/** * Only "USER" role can access url under /** */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/").permitAll(); http.authorizeRequests().antMatchers("/imgs/**").permitAll(); http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN"); http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin(); } }
Notice three things are defined here. A bean of type DaoAuthenticationProvider, a method set to use that bean and finally a overriden method to set up your own access rules based on http url.
3.4 Define class extends SecurityWebApplicationInitializer
This class will finally get the springSecurityFilterChain registered. Althought it's empty, it's mandatory!
package com.shengwang.demo; import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer { }
3.5 Load the SecurityConfig in to root context
Load the class SecurityConfig into Spring web root context.
package com.shengwang.demo; import javax.servlet.Filter; import org.springframework.web.filter.DelegatingFilterProxy; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; public class MyWebApplicationInitializerWithSpringSecurity extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { // compare to no spring security scenario // the only change is adding SecurityConfig.class return new Class[] {SecurityConfig.class,RootConfig.class}; } @Override protected Class<?>[] getServletConfigClasses() { return new Class[] {DispatcherConfig.class}; } @Override protected String[] getServletMappings() { return new String[] { "/" }; } // register other filters used in application @Override protected Filter[] getServletFilters() { return new Filter[] { new DelegatingFilterProxy("mdcInsertingServletFilter") }; } }
All done. Now the project hierrarchy may look like below.
4. Run it
Suppose we have our entity tables ready. User 'admin' has both ADMIN role and USER role. User 'tom' has only USER role.
We have simple index page as below with 3 links , first to /admin, second to /user, last one to a statice resource /imgs/sample.jpg. Just to test the access rules we set in SecurityConfig.java.
Click the last link will directly show you the image without asking you to login in.
If you click first link to /admin, a default (and ugly) login page appears ask you to login.
If we only loing as a user WITHOUT the ADMIN role, like above, the access to /admin will result 403- Access Denied.
0 comments:
Post a Comment