This is the fourth post in a series of posts to cover different aspects of Spring Boot. Please note that the entire post isn’t necessarily only written in English.
In this post, I am going to cover the basics of Spring Security.
Debug
Do not litter security-unrelated code with security code!
Break Circular Dependency:
1 | Requested bean is currently in creation: Is there an unresolvable circular reference? |
Use @Lazy
before any of the mentioned @Autowired
or @Bean
.
About Roles:
Use hasRole("User")
instead of hasRole("ROLE_USER")
, because ROLE_
is automatically added.
Basic Configuration
Enable Spring Security (the ONLY package needed for all security-related configs):
1 | <dependency> |
Spring Security starter provides the following features:
- All HTTP request paths need authentication
- No specific roles or authorities are required (Only one user, with the username
user
) - Login page made by Spring Security
Add User Store
What we need to configure:
- Prompt a customized login page
- Signup page, for multiple users
- Apply different security rules for different request paths. e.g. The homepage and signup page shouldn’t require authentication at all
Configure a user store that handles multiple users:
- In-memory
- JDBC-based
- LDAP-backed
- Custom user details service (complete customization)
Barebones config class with @Override
:
1 |
|
In-memory
Convenient for testing purposes / simple apps, but doesn’t allow easy editing of users.
1 |
|
LDAP-backed
LDAP: Lightweight Directory Access Protocol
The default strategy for authenticating against LDAP is to perform a bind operation, authenticating the user directly to the LDAP server.
Another option is to perform a comparison operation. This involves sending the entered password to the LDAP directory and asking the server to compare the password against a user’s password attribute. Because the comparison is done within the LDAP server, the actual password remains secret.
Basic Config:
- By default, users & groups base queries are empty: the search will be done from the root of LDAP hierarchy.
- With specifying the Base, rather than search from root, search will be done where the organizational unit is people.
- Add password comparison, for the comparison operation. Plus specify a different password attribute name.
1 |
|
Contact LDAP server:
The default LDAP server is on
localhost:33389
.
When the LDAP server starts, it will attempt to load data from any LDIF files that it can find in the classpath. LDIF (LDAP Data Interchange Format) is a standard way of representing LDAP data in a plain text file. Each record is composed of one or more lines, each containing a name:value
pair. Records are separated from each other by blank lines.
1 | // Add to the last line |
A sample LDIF file:
1 | dn: ou=groups,dc=tacocloud,dc=com |
JDBC-based
Maintain user info in a relational DB with JDBC.
Spring Security’s default user queries:
1 | -- WITH auth.jdbcAuthentication().dataSource(dataSource): |
Customized config:
Note: All the queries take username as the only parameter.
1 | // Set datasource, for DB access |
Encoding passwords:
1 | // Added to the last line |
Other Cryptos:
Note:
StandardPasswordEncoder
using SHA256 is deprecated after Spring Security 5.1.5.
- NoOpPasswordEncoder —Applies no encoding
- BCryptPasswordEncoder —Applies bcrypt strong hashing encryption
- Pbkdf2PasswordEncoder —Applies PBKDF2 encryption
- SCryptPasswordEncoder —Applies scrypt hashing encryption
The Password encoder interface:
1 | public interface PasswordEncoder { |
Clarification:
The password in the DB is never decoded.
The plaintext password entered by the user is encoded using the same algorithm, and then compared with the encoded password in the DB (using the matches()
method)
Custom User Detail
User Entity
1 |
|
User Repository
1 | public interface UserRepository extends CrudRepository<User, Long> { |
User Details Service
1 | public interface UserDetailsService { |
User Details Service Implementation
1 |
|
Modify SecurityConfig
1 | import org.springframework.security.core.userdetails.UserDetailsService; |
Line 21:.passwordEncoder(encoder());
It appears that we call the encoder()
method and pass its returned value to passwordEncoder()
.
However, since the encoder()
method is annotated with @Bean
, it will be used to declare a PasswordEncoder
bean in the Spring application context. Any calls to encoder()
will then be intercepted to return the bean instance from the application context.
User Controller
1 |
|
A Separate Registration Form
1 |
|
Securing Web Requests
Secure Requests
Configs with HttpSecurity
:
- Require that certain security conditions must be met before serving a request
- Configure a custom login page, and enable logout
- Configure cross-site request forgery protection (CSRF)
Specify 2 security rules:
- Requests for
/design
and/orders
should be for users with a granted authority ofROLE_USER
- All requests should be permitted to all users
1 | // HttpSecurity object |
Important:
The order of the rules are important. Security rules declared first take precedence over those declared lower down.
If we reverse the order, all requests would have permitAll()
applied to them, and the rule for /design
and /orders
requests would have no effect.
Spring Security’s config methods for securing paths:
SpEL: Spring Expression Language
Method | What It Does |
---|---|
access(String) |
Allows access if the given SpEL expression evaluates to true |
rememberMe() |
Allows access for users who are authenticated via remember-me |
permitAll() |
Allows access unconditionally |
denyAll() |
Denies access unconditionally |
. | |
authenticated() |
Allows access to authenticated users |
fullyAuthenticated() |
Allows access if the user is fully authenticated (not remembered) |
hasAnyAuthority(String...) |
Allows access if the user has any of the given authorities |
hasAnyRole(String...) |
Allows access if the user has any of the given roles |
hasAuthority(String) |
Allows access if the user has the given authority |
hasIpAddress(String) |
Allows access if the request comes from the given IP address |
hasRole(String) |
Allows access if the user has the given role |
. | |
not() |
Negates the effect of any of the other access methods |
anonymous() |
Allows access to anonymous users |
Spring Security Extension to SpEL
Security Expression | What It Evaluates |
---|---|
authentication |
The user’s authentication object |
isRememberMe() |
true if the user was authenticated via remember-me |
denyAll |
Always evaluates to false |
permitAll |
Always evaluates to true |
. | |
isAuthenticated() |
true if the user is authenticated |
isFullyAuthenticated() |
true if the user is fully authenticated (not with remember-me) |
hasAnyRole(list of roles) |
true if the user has any of the given roles |
hasRole(role) |
true if the user has the given role |
hasIpAddress(IP address) |
true if the request comes from the given IP address |
. | |
isAnonymous() |
true if the user is anonymous |
principal |
The user’s principal object |
Example:
Only allow authenticated user to place order on Tuesday:
1 |
|
Custom Login & Logout
Add a ViewController Path in WebConfig:
1 |
|
Add to SecurityConfig
:
By default, Spring Security listens for login requests at
/login
, and expects that the username and password fields be namedusername
andpassword
.But we can change it through the config below:
1 | http.authorizeRequests() |
Logging Out
1 | // 末尾加上这行 |
CSRF
Spring Security has built-in CSRF protection, and it’s enabled by default.
Cross-site request forgery (CSRF) is a common security attack. It involves subjecting a user to visit a maliciously designed web page that automatically & secretly submits a form to another application on behalf of a user. For instance, a user may be presented with a form on an attacker’s website that automatically posts to a URL on the user’s banking website to transfer money.
To protect against such attacks, applications can generate a CSRF token upon displaying a form, place that token in a hidden field, and then save it for later use on the server. When the form is submitted, the token is sent back to the server along with the rest of the form data. The request is then intercepted by the server and compared with the token that was originally generated. If the token matches, the request is allowed to proceed.
Know Your User
Link User with Order:
Information about the authenticated user can be obtained via the
SecurityContext
object, or injected into controllers using@AuthenticationPrincipal
.
Add this to the Order
entity:
1 |
|
Determine which user is associated with which order(s):
- Inject a
Principal
object into the controller method - Inject an
Authentication
object into the controller method - Use
SecurityContextHolder
to get at the security context - Use an
@AuthenticationPrincipal
annotated method
1 |
|
上面的方法是没问题的,但这个是 Bad Practice!
Do not litter security-unrelated code with security code!
方法一(Cleanest Solution):
1 | // @AuthPrincipal limits the security-specific code to the annotation itself |
方法二:
1 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); |
此方法的好处是:
It can be used anywhere in the application, not only in a controller’s handler methods. This makes it suitable for use in lower levels of the code.