spring-security登录和权限管理

spring security

spring security 主要的两个功能是认证和授权
认证的大概流程:
Username password AuthenticationFilter(自定义usernamepassword拦截器)
UserDetailService (查询用户密码的service接口)
Userdetails (用户类接口)
AuthenticationProvide (为认证管理器AuthenticationManager 提供验证组件AuthenticationProvider)
授权的大概流程:
(extends)AbstractsecurityInterceptor +(implements)Filter(资源访问过滤器,拦截访问请求,封装成安全对象FilterInvocation,调用前两个实例进行鉴权)
FilterInvocationSecurityMetadataSource(自定义权限数据源,提供所有URL资源与对应角色权限的映射集合)
AccessDecisionManager (自定义鉴权管理器,根据URL资源权限和用户角色权限进行鉴权)

用户登陆
会被AuthenticationProcessingFilter拦截,调用AuthenticationManager的实现,而且AuthenticationManager会调用ProviderManager来获取用户验证信息(不同的Provider调用的服务不同,因为这些信息可以是在数据库上,可以是在LDAP服务器上,可以是xml配置文件上等),如果验证通过后会将用户的权限信息封装一个User放到spring的全局缓存SecurityContextHolder中,以备后面访问资源时使用。
访问资源(即授权管理
访问url时,会通过AbstractSecurityInterceptor拦截器拦截,其中会调用FilterInvocationSecurityMetadataSource的方法来获取被拦截url所需的全部权限,在调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则返回,权限不够则报错并调用权限不足页面。

项目结构:
在这里插入图片描述
数据库设计:
在这里插入图片描述
model:
Permission

package com.example.arcgisdemo.model;

import com.sun.javafx.beans.IDProperty;

import javax.persistence.*;
import java.util.List;

@Entity
@Table(name = "SYS_PERMISSION")
public class Permission {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    private int id;
    @Column(name = "NAME")
    private String name;
    @Column(name = "DESCRIPTION")
    private String description;
    @Column(name = "URL")
    private String url;
    @Column(name = "PID")
    private String pid;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getPid() {
        return pid;
    }

    public void setPid(String pid) {
        this.pid = pid;
    }

}

User:
这里是在User中实现了UserDetails

package com.example.arcgisdemo.model;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Entity
@Table(name = "SYS_USER")
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    private int id;
    @Column(name = "USERNAME")
    private String username;
    @Column(name = "PASSWORD")
    private String password;
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "SYS_ROLE_USER",
            joinColumns = {@JoinColumn(name = "SYS_USER_ID",referencedColumnName = "ID")},
            inverseJoinColumns = {@JoinColumn(name = "SYS_ROLE_ID",referencedColumnName = "ID")})
    private List<Role> roles;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (roles == null || roles.size() < 1) {
            return AuthorityUtils.commaSeparatedStringToAuthorityList("");
        }
        StringBuilder rolestring = new StringBuilder();
        for (Role role : roles) {
            rolestring.append(role.getName()).append(",");
    }
        List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(rolestring.substring(0, rolestring.length() - 1));
        return authorityList;
    }


    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

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

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

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

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }



}

Role

package com.example.arcgisdemo.model;

import javax.persistence.*;
import java.util.List;

@Entity
@Table(name = "SYS_ROLE")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    private int id;
    @Column(name = "NAME")
    private String name;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "SYS_PERMISSION_ROLE",
                joinColumns = {@JoinColumn(name = "ROLE_ID",referencedColumnName = "ID")},
                inverseJoinColumns = {@JoinColumn(name = "PERMISSION_ID",referencedColumnName = "ID")})
    private List<Permission> permissions;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Permission> getPermissions() {
        return permissions;
    }

    public void setPermissions(List<Permission> permissions) {
        this.permissions = permissions;
    }
}

security:
WebSecurityConfig 配置

package com.example.arcgisdemo.security;

import com.example.arcgisdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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 org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity //注解开启Spring Security的功能
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启Spring Security注解功能
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserService userService;
    @Autowired
    private SysFilterSecurityInterceptor sysFilterSecurityInterceptor;
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
//        auth.authenticationProvider(new SysDaoAuthenticationProvider());
    }

    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/js/layui/**").permitAll() //定义不需要认证就可以访问的资源
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")  //定义当需要用户登录时候,转到的登录页面
                .loginProcessingUrl("/login")
                .defaultSuccessUrl("/", true)
                .failureUrl("/login?error").permitAll()
                .and()
                .logout()
                .logoutUrl("/logout")
                //退出登录后的默认Url是login
                .logoutSuccessUrl("/login")
                .permitAll();
        //解决非thymeleaf的form表单提交被拦截问题
        http.csrf().disable();
        http.addFilter(customUsernamePasswordAuthenticationFilter());
        http.addFilterBefore(sysFilterSecurityInterceptor, FilterSecurityInterceptor.class)
            .csrf().disable();
        http.headers().frameOptions().sameOrigin();
    }
/*,
        "https://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Hydrography/Watershed173811/MapServer/1",
        "https://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Hydrography/Watershed173811/MapServer/0",
        "http://192.168.101.4:8080/agapi/**"*/
    @Bean
    public static NoOpPasswordEncoder passwordEncoder() {
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public UserDetailsService systemUserService() {
        return new UserService();
    }

    @Bean
    public SysUsernamePasswordF customUsernamePasswordAuthenticationFilter() throws Exception {
        SysUsernamePasswordF customUsernamePasswordAuthenticationFilter = new SysUsernamePasswordF();
        customUsernamePasswordAuthenticationFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
        customUsernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
        customUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(new SavedRequestAwareAuthenticationSuccessHandler());
        customUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/login?error"));
        return customUsernamePasswordAuthenticationFilter;
    }
}

SysUsernamePasswordF

package com.example.arcgisdemo.security;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

public class SysUsernamePasswordF extends UsernamePasswordAuthenticationFilter {

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }else {

            }

            if (password == null) {
                password = "";
            }else {

            }

            username = username.trim();
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
}

SysUserDetailsService

package com.example.arcgisdemo.security;

import ch.qos.logback.core.joran.conditional.ElseAction;
import com.example.arcgisdemo.model.Permission;

import com.example.arcgisdemo.model.User;
import com.example.arcgisdemo.service.PermissionService;
import com.example.arcgisdemo.service.SysUserService;
import com.example.arcgisdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.ArrayList;
import java.util.List;

public class SysUserDetailsService implements UserDetailsService {

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private PermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String username)  {
        User user = sysUserService.findByUserName(username);
        if (user != null) {
            List<Permission> permissions = permissionService.findById(user.getId());
            List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
            for (Permission permission : permissions) {
                if (permission != null && permission.getName() != null) {
                    GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getName());
                    //将此处权限信息添加到GrantedAuthority对象中,在后面进行全权限验证时会使用GrantedAuthority
                    grantedAuthorities.add(grantedAuthority);
                }
            }
            return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities);
        }else {
            throw new UsernameNotFoundException("do not exist");
        }
    }
}

SysInvocationSecurityMetadataSourceService

package com.example.arcgisdemo.security;

import com.example.arcgisdemo.model.Permission;
import com.example.arcgisdemo.service.PermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;
import sun.security.krb5.Config;

import javax.servlet.http.HttpServletRequest;
import java.util.*;

@Service
public class SysInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {

    @Autowired
    private PermissionService permissionService;

    private HashMap<String,Collection<ConfigAttribute>> map=null;

    //加载权限表中所有权限
    public void loadResourceDefine(){
        map=new HashMap<>();
        Collection<ConfigAttribute> array;
        ConfigAttribute cfg;
        List<Permission> permissions=permissionService.findAll();
        for (Permission permission:permissions){
            array=new ArrayList<>();
            cfg=new SecurityConfig(permission.getName());
            //此处指添加用户的名字,其实可以添加更多权限信息。例如请求方法到ConfigAttributr的集合中
            array.add(cfg);
            //用权限的getUrl。作为map的key。用ConfigAttribute的集合作为value
            map.put(permission.getUrl(),array);
        }
    }
//此方法是为了判定用户请求的url是否在权限表中,如果在权限表中,则返回给decide()方法,
// 用来判断用户是否有此权限,如果不在权限表中则放行

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
     if(map ==null)loadResourceDefine();
     //object中包含用户请求的request信息
        HttpServletRequest request=((FilterInvocation)object).getHttpRequest();
        AntPathRequestMatcher matcher;
        String resUrl;
        for (Iterator<String> iter=map.keySet().iterator();iter.hasNext();){
            resUrl=iter.next();
            matcher=new AntPathRequestMatcher(resUrl);
            if (matcher.matches(request)){
                return map.get(resUrl);
            }
        }
        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

SysFilterSecurityInterceptor

package com.example.arcgisdemo.security;

import org.apache.catalina.connector.Request;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Service;
import javax.servlet.*;

import javax.servlet.*;
import java.io.IOException;
@Service(value = "sysFilterSecurityInterceptor")
public class SysFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

    @Autowired
    @Qualifier(value = "sysInvocationSecurityMetadataSourceService")
    private SysInvocationSecurityMetadataSourceService sysInvocationSecurityMetadataSourceService;

    @Autowired
    @Qualifier(value = "sysAccessDecisionManager")
    public void setSysAccessDecisionManager(SysAccessDecisionManager sysAccessDecisionManager){
        super.setAccessDecisionManager(sysAccessDecisionManager);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @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{
        //fi里面有一个被拦截的url
        //里面调用SysInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
        //在调用SysAccessDecisionManager的decide方法来校验用户的权限是否足够
        InterceptorStatusToken token=super.beforeInvocation(fi);
        try {
            //执行下一个拦截器
            fi.getChain().doFilter(fi.getRequest(),fi.getResponse());
        }finally {
            super.afterInvocation(token,null);
        }
    }
    @Override
    public void destroy() {

    }

    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.sysInvocationSecurityMetadataSourceService;
    }
}

SysAccessDecisionManager

package com.example.arcgisdemo.security;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.Iterator;
@Service(value = "sysAccessDecisionManager")
public class SysAccessDecisionManager implements AccessDecisionManager {

    //decide方法是判定是否拥有权限的决策方法
    //authentication  是释SysUserDetailsService中循环添加到GrantedAuthority对象中的权限信息集合
    //object 包含客户端发起的请求的request信息。可转换为HttpServlerRequest request=((FilterInvocation) object).getHttpRequest();
    //configAttributes 为InvocationSecurityMetadataSource的getAttributes(Object)这个方法返回的结果
    //此方法是为了判定用户请求的url,是否在权限表中,如果在权限表中,则返回给decide方法,
    //用来判定用户是否有此权限,如果不在权限表中则放行。
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        if (null==configAttributes || configAttributes.size()<=0){
            return;
        }
        ConfigAttribute configAttribute;
        String needRole;
        for (Iterator<ConfigAttribute> iter=configAttributes.iterator();iter.hasNext();){
            configAttribute=iter.next();
            needRole=configAttribute.getAttribute();
            for (GrantedAuthority ga:authentication.getAuthorities()){
                if (needRole.trim().equals(ga.getAuthority())){
                    return;
                }
            }
        }
        throw new AccessDeniedException("no right");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

Service:
PermissionService

package com.example.arcgisdemo.service;

import com.example.arcgisdemo.dao.PermissionDao;
import com.example.arcgisdemo.model.Permission;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import java.util.List;

@Service(value = "permissionService")
public class PermissionService {
    @Autowired
    @Qualifier(value = "permissionDao")
    private PermissionDao permissionDao;

    public List<Permission> findAll(){
        return permissionDao.findAll();
    }

    public List<Permission> findById(int id){
        return permissionDao.findById(id);
    }
}

SysUserService

package com.example.arcgisdemo.service;

import com.example.arcgisdemo.dao.UserMapper;
import com.example.arcgisdemo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.util.List;

@Service(value = "sysUserService")
public class SysUserService {

    @Autowired
    @Qualifier(value = "userMapper")
    private UserMapper userMapper;

    public User queryByUserName(String username) {
        return userMapper.queryByUsername(username);
    }
    public User findByUserName(String username){
        return userMapper.findByUsername(username);
    }
}

UserService

package com.example.arcgisdemo.service;

import com.example.arcgisdemo.model.User;
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;
@Service(value = "userService")
public class UserService implements UserDetailsService {

    @Autowired
    private SysUserService systemUserService;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //根据用户名从数据库查询对应记录
        User user=systemUserService.queryByUserName(s);
        if (user ==null){
            throw new UsernameNotFoundException("username is not exists");
        }
        System.out.println("username:"+user.getUsername()+",password:"+user.getPassword());
        return user;
    }
}

dao:
PermissionDao

package com.example.arcgisdemo.dao;

import com.example.arcgisdemo.model.Permission;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository(value = "permissionDao")
public interface PermissionDao extends JpaRepository<Permission,Long> {
    List<Permission> findAll();
    List<Permission> findById(int id);
}

UserMapper

package com.example.arcgisdemo.dao;

import com.example.arcgisdemo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository(value = "userMapper")
public interface UserMapper extends JpaRepository<User,Long> {
     User queryByUsername(String username);

    User findByUsername(String username);
}

controller

package com.example.arcgisdemo.controller;

import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class DemoController {
    @RequestMapping("/login")
    public String login() {
        return "login";
    }

    @RequestMapping("/")
    public String index1() {
        return "index-ui";
    }

/*@Secured({"ROLE_ADMIN"})*/
    @RequestMapping("/user")
    public String user(){
        return "user";
    }

    @RequestMapping("/map")
    public String map(){
        return "map";
    }
}

也可以在页面上设置权限,让没有权限的用户看不到该功能

<div sec:authorize="hasRole('ADMIN')"><!--设置权限-->
      <ul class="layui-nav layui-layout-left">
        <li class="layui-nav-item"><a href="">控制台</a></li>
        <li class="layui-nav-item"><a href="">管理</a></li>

        <li class="layui-nav-item"><a href="/user">用户</a></li>
        <li class="layui-nav-item">
          <a href="javascript:;">其它系统</a>
          <dl class="layui-nav-child">
            <dd><a href="">邮件管理</a></dd>
            <dd><a href="">消息管理</a></dd>
            <dd><a href="">授权管理</a></dd>
          </dl>
        </li>
      </ul>
    </div>

在这里遇到个问题 刚开始的时候没有效果,经过找资料需要将spring 版本降到2.0.7以下
再加上:

<html
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">

pom.xml 需要配置secutiry扩展包

<dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity4</artifactId>
            <version>3.0.2.RELEASE</version>
        </dependency>

参考文档:链接:https://www.jianshu.com/p/bcbbf16610fb


版权声明:本文为qq_44907404原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。