Introduction to the
Spring Security, a Security framework based on Spring AOP and Servlet filters. It provides a comprehensive security solution that handles authentication and authorization at both the Web request level and method invocation level.
The working process
I found a working flow chart of Spring Security from the Internet, as follows.
Quick learning
Build table
Table structure
DROP TABLE IF EXISTS `user`;
DROP TABLE IF EXISTS `role`;
DROP TABLE IF EXISTS `user_role`;
DROP TABLE IF EXISTS `role_permission`;
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `user` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL.`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`));CREATE TABLE `role` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`));CREATE TABLE `user_role` (
`user_id` bigint(11) NOT NULL.`role_id` bigint(11) NOT NULL
);
CREATE TABLE `role_permission` (
`role_id` bigint(11) NOT NULL.`permission_id` bigint(11) NOT NULL
);
CREATE TABLE `permission` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`url` varchar(255) NOT NULL.`name` varchar(255) NOT NULL.`description` varchar(255) NULL.`pid` bigint(11) NOT NULL,
PRIMARY KEY (`id`));INSERT INTO user (id, username, password) VALUES (1.'user'.'e10adc3949ba59abbe56e057f20f883e');
INSERT INTO user (id, username , password) VALUES (2.'admin'.'e10adc3949ba59abbe56e057f20f883e');
INSERT INTO role (id.name) VALUES (1.'USER');
INSERT INTO role (id.name) VALUES (2.'ADMIN');
INSERT INTO permission (id.url.name, pid) VALUES (1.'/user/common'.'common'.0);
INSERT INTO permission (id.url.name, pid) VALUES (2.'/user/admin'.'admin'.0);
INSERT INTO user_role (user_id, role_id) VALUES (1.1);
INSERT INTO user_role (user_id, role_id) VALUES (2.1);
INSERT INTO user_role (user_id, role_id) VALUES (2.2);
INSERT INTO role_permission (role_id, permission_id) VALUES (1.1);
INSERT INTO role_permission (role_id, permission_id) VALUES (2.1);
INSERT INTO role_permission (role_id, permission_id) VALUES (2.2);
Copy the code
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-security4</artifactId>
</dependency>
Copy the code
application.yml
spring:
thymeleaf:
mode: HTML5
encoding: UTF-8
cache: false
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring-security? useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
Copy the code
User
public class User implements UserDetails , Serializable {
private Long id;
private String username;
private String password;
private List<Role> authorities;
public Long getId(a) {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public String getUsername(a) {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String getPassword(a) {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public List<Role> getAuthorities(a) {
return authorities;
}
public void setAuthorities(List<Role> authorities) {
this.authorities = authorities;
}
/** * Whether the user account expires */
@Override
public boolean isAccountNonExpired(a) {
return true;
}
/** * Whether the account is locked */
@Override
public boolean isAccountNonLocked(a) {
return true;
}
/** * Whether the user password has expired */
@Override
public boolean isCredentialsNonExpired(a) {
return true;
}
/** * Whether the user is available */
@Override
public boolean isEnabled(a) {
return true; }}Copy the code
The User class above implements the UserDetails interface, which is the core interface for implementing Spring Security authentication information. GetUsername is the method of the UserDetails interface. This method returns username as well as other user information, such as mobile phone number and email address. Orities’ getAuthorities() method returns information about the permissions set by the user. In this example, it mimics retrieving all of the user’s role information from a database. The permissions can also be other information about the user, not necessarily the role. In addition, you need to read the password. The last few methods generally return true. You can also make business judgments based on your own requirements.
Role
public class Role implements GrantedAuthority {
private Long id;
private String name;
public Long getId(a) {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName(a) {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String getAuthority(a) {
returnname; }}Copy the code
The Role class implements the GrantedAuthority interface and overwrites the getAuthority() method. The permission point can be any string, not necessarily the role name.
All Authentication implementation classes keep a list of GrantedAuthorities that represent the permissions the user has. GrantedAuthority is set to the Authentication object through AuthenticationManager, The AccessDecisionManager then obtains the user’s GrantedAuthority from Authentication to verify that the user has access to the corresponding resource.
MyUserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
// Check the database
User user = userMapper.loadUserByUsername( userName );
if (null! = user) { List<Role> roles = roleMapper.getRolesByUserId( user.getId() ); user.setAuthorities( roles ); }returnuser; }}Copy the code
The Service layer needs to implement the UserDetailsService interface, which obtains all information of the user based on the user name, including the user information and permission point.
MyInvocationSecurityMetadataSourceService
@Component
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
@Autowired
private PermissionMapper permissionMapper;
/** ** The role required for each resource Collection
is used by the decision maker
private static HashMap<String, Collection<ConfigAttribute>> map =null;
/** * returns the role required for the requested resource */
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
if (null == map) {
loadResourceDefine();
}
// Object contains the request information requested by the user
HttpServletRequest request = ((FilterInvocation) o).getHttpRequest();
for (Iterator<String> it = map.keySet().iterator() ; it.hasNext();) {
String url = it.next();
if (new AntPathRequestMatcher( url ).matches( request )) {
returnmap.get( url ); }}return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes(a) {
return null;
}
@Override
public boolean supports(Class
aClass) {
return true;
}
/** * Initializes roles for all resources */
public void loadResourceDefine(a) {
map = new HashMap<>(16);
// The intermediate table of role permissions is the table corresponding to permission resources and roles
List<RolePermisson> rolePermissons = permissionMapper.getRolePermissions();
// Which roles can access a resource
for (RolePermisson rolePermisson : rolePermissons) {
String url = rolePermisson.getUrl();
String roleName = rolePermisson.getRoleName();
ConfigAttribute role = new SecurityConfig(roleName);
if(map.containsKey(url)){
map.get(url).add(role);
}else{
List<ConfigAttribute> list = newArrayList<>(); list.add( role ); map.put( url , list ); }}}}Copy the code
MyInvocationSecurityMetadataSourceService class implements the FilterInvocationSecurityMetadataSource, FilterInvocationSecurityMetadataSource function is used to store requests corresponding relationship with permissions.
FilterInvocationSecurityMetadataSource interface has three methods:
- boolean supports(Class
clazz) : indicates whether the class can provide ConfigAttributes for a specified method call or Web request. - Collection getAllConfigAttributes() : This method is called automatically when the Spring container starts. The mapping between all requests and permissions is also initialized in this method, stored in a property variable.
- Collection getAttributes (Object Object) : when receives an HTTP request, filterSecurityInterceptor invokes the method. The object argument is an instance of HttpServletRequest that contains URL information. This method returns the set of all permissions required to request the URL.
MyAccessDecisionManager
/** * decision */
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
private final static Logger logger = LoggerFactory.getLogger(MyAccessDecisionManager.class);
/** * Determines whether the user has permission to access the corresponding protected object by passing the parameter **@paramAuthentication contains the information about the current user, including the permissions. The source of the permission is the authorities set in UserDetailsService during the previous login. *@paramThe Object is the FilterInvocation object, which generates Web resources such as Request *@paramConfigAttributes configAttributes is the permission required for this access */
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if (null == configAttributes || 0 >= configAttributes.size()) {
return;
} else {
String needRole;
for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
needRole = iter.next().getAttribute();
for(GrantedAuthority ga : authentication.getAuthorities()) {
if(needRole.trim().equals(ga.getAuthority().trim())) {
return; }}}throw new AccessDeniedException("No current access permission"); }}/** * indicates whether this AccessDecisionManager can handle the authorization request */ presented by the passed ConfigAttribute
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
/** * indicates whether the current AccessDecisionManager implementation can provide access control decisions for the specified security object (method call or Web request) */
@Override
public boolean supports(Class
aClass) {
return true; }}Copy the code
MyAccessDecisionManager class implements the AccessDecisionManager interface, the AccessDecisionManager is by the AbstractSecurityInterceptor calls, It is responsible for authenticating whether the user has access to the corresponding resource (method or URL).
MyFilterSecurityInterceptor
@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
// Execute the next interceptor
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null); }}@Override
publicClass<? > getSecureObjectClass() {return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource(a) {
return this.securityMetadataSource; }}Copy the code
Each supported security object types (method calls or Web request) has its own interceptor class, it is a subclass of AbstractSecurityInterceptor, AbstractSecurityInterceptor is an implementation of a visit to the protected object to intercept an abstract class.
The AbstractSecurityInterceptor mechanism can be divided into several steps:
-
- Find “configuration properties” associated with the current request
-
- Submit the security object (method call or Web request), current authentication, and configuration properties to the AccessDecisionManager
-
- (Optional) Change the authentication on which the call is based
-
- Allows security object calls to continue (assuming access is granted)
-
- After the call returns, if AfterInvocationManager is configured. AfterInvocationManager is not called if the call throws an exception.
Methods on the AbstractSecurityInterceptor description:
- The beforeInvocation() method verifies access to the protected object. AccessDecisionManager and AuthenticationManager are used internally.
- The finallyInvocation() method is used for some cleanup after the invocation of the protected object, mainly if the SecurityContext is changed in the beforeInvocation() invocation, In the finallyInvocation() it needs to be restored to the original SecurityContext, and the invocation should be included in the finally statement block when the subclass invocation requests a protected resource.
- The afterInvocation() method handles the returned result and its Decide () method is called by default if AfterInvocationManager is injected.
To understand the AbstractSecurityInterceptor, you should understand, We want to use custom MyFilterSecurityInterceptor is our custom before the AccessDecisionManager and securityMetadataSource.
SecurityConfig
@ EnableWebSecurity annotations and WebSecurityConfigurerAdapter together provide web based security. Custom class Inherited WebSecurityConfigurerAdapter to rewrite some of the ways to specify some particular Web security Settings.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService userService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// Verify the user
auth.userDetailsService( userService ).passwordEncoder( new PasswordEncoder() {
// Encrypt the password
@Override
public String encode(CharSequence charSequence) {
System.out.println(charSequence.toString());
return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
}
// Match the password
@Override
public boolean matches(CharSequence charSequence, String s) {
String encode = DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
boolean res = s.equals( encode );
returnres; }}); }@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/"."index"."/login"."/login-error"."/ 401"."/css/**"."/js/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage( "/login" ).failureUrl( "/login-error" )
.and()
.exceptionHandling().accessDeniedPage( "/ 401" );
http.logout().logoutSuccessUrl( "/"); }}Copy the code
MainController
@Controller
public class MainController {
@RequestMapping("/")
public String root(a) {
return "redirect:/index";
}
@RequestMapping("/index")
public String index(a) {
return "index";
}
@RequestMapping("/login")
public String login(a) {
return "login";
}
@RequestMapping("/login-error")
public String loginError(Model model) {
model.addAttribute( "loginError" , true);
return "login";
}
@GetMapping("/ 401")
public String accessDenied(a) {
return "401";
}
@GetMapping("/user/common")
public String common(a) {
return "user/common";
}
@GetMapping("/user/admin")
public String admin(a) {
return "user/admin"; }}Copy the code
page
login.html
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>The login</title>
</head>
<body>
<h1>Login page</h1>
<p th:if="${loginError}" class="error">The user name or password is incorrect</p>
<form th:action="@{/login}" method="post">
<label for="username">The user name</label>:
<input type="text" id="username" name="username" autofocus="autofocus" />
<br/>
<label for="password">Secret code</label>:
<input type="password" id="password" name="password" />
<br/>
<input type="submit" value="Login" />
</form>
<p><a href="/index" th:href="@{/index}"></a></p>
</body>
</html>
Copy the code
index.html
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<head>
<meta charset="UTF-8">
<title>Home page</title>
</head>
<body>
<h2>page list</h2>
<a href="/user/common">common page</a>
<br/>
<a href="/user/admin">admin page</a>
<br/>
<form th:action="@{/logout}" method="post">
<input type="submit" class="btn btn-primary" value="Cancel"/>
</form>
</body>
</html>
Copy the code
admin.html
<head>
<meta charset="UTF-8">
<title>admin page</title>
</head>
<body>Success Admin Page!!</body>
</html>
Copy the code
common.html
<head>
<meta charset="UTF-8">
<title>common page</title>
</head>
<body>Success Common Page!!</body>
</html>
Copy the code
401.html
<html lang="en">
<head>
<meta charset="UTF-8">
<title>401 page</title>
</head>
<body>
<div>
<div>
<h2>Not enough permissions</h2>
<p>Access denied!</p>
</div>
</div>
</body>
</html>
Copy the code
Finally, run the project, you can use the user account, admin account to test whether the authentication and authorization is correct.
reference
Deep Understanding of Spring Cloud and Microservice Building
www.ktanx.com/blog/p/4929
The source code
Github.com/gf-huanchup…