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:

    1. Find “configuration properties” associated with the current request
    1. Submit the security object (method call or Web request), current authentication, and configuration properties to the AccessDecisionManager
    1. (Optional) Change the authentication on which the call is based
    1. Allows security object calls to continue (assuming access is granted)
    1. 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…