目录
Spring Security核心功能
关于安全方面的两个主要区域是认证和授权(或者访问控制),这两点也是Spring Security核心功能
用户认证
:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
用户授权
:验证谋而用户是否有权限执行某一操作。在一个系统中,不同用户所具有的权限是不同的。
Spring Security入门案例
1.创建SpringBoot工程
securitydemo
2.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3.编写controller测试
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("hello")
public String add(){
return "hello security";
}
}
server.port=8111
直接访问会发现需要登录:http://localhost:8111/test/hello
默认用户:user
密码在启动日志里有打印
Spring Security基本原理
Spring Security本质是一个过滤器链
项目启动时默认加载的有如下过滤器:
Creating filter chain: any request,
[org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@4fe875be,
org.springframework.security.web.context.SecurityContextPersistenceFilter@d1a10ac,
org.springframework.security.web.header.HeaderWriterFilter@41aaedaa,
org.springframework.security.web.csrf.CsrfFilter@73877e19,
org.springframework.security.web.authentication.logout.LogoutFilter@27f74733,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@27b71f50,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@198ef2ce,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@677b8e13,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@7a24eb3,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@31fc71ab,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@3005db4a,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@4a9486c0,
org.springframework.security.web.session.SessionManagementFilter@75b3673,
org.springframework.security.web.access.ExceptionTranslationFilter@7068f7ca,
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@5f404594
举例:
FilterSecurityInterceptor
:是一个方法级的权限过滤器,位于过滤器的最底层。
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
Filter
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
==============================invoke(fi);===================
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
//如果之前有过滤器放行
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//执行过滤器本身
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
ExceptionTranslationFilter
:异常过滤器,用来处理在认证授权中抛出的异常。
可以发现chain.doFilter(request, response);后面都是异常处理。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
chain.doFilter(request, response);
logger.debug("Chain processed normally");
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
if (ase != null) {
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
}
handleSpringSecurityException(request, response, chain, ase);
}
else {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
}
}
UsernamePasswordAuthenticationFilter
:对login的POST请求做拦截,校验表单中用户名,密码。
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
Spring Security过滤器如何进行加载
1.使用Spring Security配置过滤器
DelegatingFilterProxy:
doFilter----->initDelegate(wac);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
//注意这里
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
// Let the delegate perform the actual doFilter operation.
invokeDelegate(delegateToUse, request, response, filterChain);
}
==============================initDelegate(wac);==========================
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
String targetBeanName = getTargetBeanName(); targetBeanName=FilterChainProxy
Assert.state(targetBeanName != null, "No target bean name set");
Filter delegate = wac.getBean(targetBeanName, Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
FilterChainProxy的核心方法,加载过滤器
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);
//这里拿到了所有的过滤器
List<Filter> filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
+ (filters == null ? " has no matching filters"
: " has an empty filter list"));
}
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
return;
}
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}
Spring Security基本原理,两个核心接口:
1.UserDetailsService:查询数据库里的用户名和密码
我们如果要自己写校验用户名密码,我们需要继承UsernamePasswordAuthenticationFilter,并重写它的attemptAuthentication
方法(校验过程),和它父类AbstractAuthenticationProcessingFilter
的successfulAuthentication
方法(校验成功怎么做)和unsuccessfulAuthentication
(校验失败怎么做)。但是我们实际项目在做校验时,用户名和密码是从数据库里查的,这个就需要用到UserDetailsService
接口。我们需要自己写个类实现UserDetailsService
并重写其loadUserByUsername
方法,编写查询数据库过程,返回User
对象,这个User对象时SpringSecurity框架提供的。
2.PasswordEncoder 给密码加密
用于User返回对象里面的密码加密
public interface PasswordEncoder {
/**
* Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
* greater hash combined with an 8-byte or greater randomly generated salt.
*/
String encode(CharSequence rawPassword);
/**
* Verify the encoded password obtained from storage matches the submitted raw
* password after it too is encoded. Returns true if the passwords match, false if
* they do not. The stored password itself is never decoded.
*
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* @return true if the raw password, after encoding, matches the encoded password from
* storage
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
/**
* Returns true if the encoded password should be encoded again for better security,
* else false. The default implementation always returns false.
* @param encodedPassword the encoded password to check
* @return true if the encoded password should be encoded again for better security,
* else false.
*/
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
Web项目权限控制方案
1.设置用户名和密码的三种方式:
① 通过配置文件
② 配置类
③ 自定义编写实现类
1.1通过配置文件
spring.security.user.name=user
spring.security.user.password=123456
1.2通过配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("123456");
auth.inMemoryAuthentication().withUser("user").password(encode).roles("admin");
}
@Bean
public PasswordEncoder createPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
上述两种方式实际项目中并不实用
1.3自定义编写实现类
1 创建配置类,设置使用哪个UserDetailService的实现类
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder( createPasswordEncoder());
}
@Bean
public PasswordEncoder createPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
2 编写实现类,返回User对象,User对象有用户名密码和操作权限
@Configuration("userDetailsService")
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User("Tom", new BCryptPasswordEncoder().encode("123456"), auths);
}
}
上述只是为了演示,还并未去查数据库。
2实现数据库查询来完成用户认证过程
2.1 整合MybatisPlus
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
2.2创建数据库表
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(20) NOT NULL,
`password` varchar(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
2.3编写实体类
@Data
public class Users {
private Long id;
private String username;
private String passowrd;
}
UserMapper
public interface UserMapper extends BaseMapper<Users> {
}
2.4在UserDetailsService 调用mapper里面的方法查询数据库进行用户认证。
@Service("userDetailsService")
public class MyUserDetailService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<Users> wrapper = new QueryWrapper<>();
wrapper.eq(username, username);
Users users = userMapper.selectOne(wrapper);
if(users == null){
throw new UsernameNotFoundException("用户名不存在");
}else{
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User(username, new BCryptPasswordEncoder().encode(users.getPassowrd()), auths);
}
}
}
2.5 启动类加注解
@MapperScan("com.hc.securitydemo.mapper")
@SpringBootApplication
public class SecuritydemoApplication {
public static void main(String[] args) {
SpringApplication.run(SecuritydemoApplication.class, args);
}
}
2.6配置数据源
#?serverTimezone=GMT%2B8 mysql8之后默认要加时区
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springsecurity/user
spring.datasource.username=root
spring.security.user.password=123456
用户自定义登录页面 登录页面不需要认证就可以访问
1.配置类修改
配置类同样是继承WebSecurityConfigurerAdapter
,但重写的方法还有protected void configure(HttpSecurity http) throws Exception,之前是public void configure(WebSecurity web),注意方法参数不同。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //自定义自己编写的登录页面
.loginPage("login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登录访问路径 注意是controller的路径
.defaultSuccessUrl("test/index").permitAll() //登录成功后跳转到的路径
.and().authorizeRequests().antMatchers("/","/test/hello","/user/login").permitAll() //设置哪些路径不需要认证
.anyRequest().authenticated() //表示所有路径都能访问 无需认证
.and().csrf().disable(); //关闭csrf保护
}
2.创建相关页面
login.html,注意里面的username和password必须是这两个名字,不然spring security识别不到。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login" method="post">
用户名:<input type="text" name="username">
<br>
密码:<input type="password" name="password">
<input type="submit" value="login">
</form>
</body>
</html>
注意里面的username和password必须是这两个名字,不然spring security识别不到,这是在UsernamePasswordAuthenticationFilter
源码里写死的
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =====================================================================================
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
新增Controller
@GetMapping("index")
public String index(){
return "hello index";
}
基于角色和权限进行访问控制
1.hasAuthority方法(一个)
如果当前的主体具有指定的权限
,则返回true,否则返回false
1.1 在配置类设置当前访问地址有哪些权限
.antMatchers("/test/index").hasAnyAuthority("admins" ) //当前登录用户必须要有admins权限才能访问
1.2在UserDetailsService中把返回User对象设置权限
public class MyUserDetailService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<Users> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
Users users = userMapper.selectOne(wrapper);
if(users == null){
throw new UsernameNotFoundException("用户名不存在");
}else{
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
return new User(username, new BCryptPasswordEncoder().encode(users.getPassword()), auths);
}
}
}
2.hasAnyAuthority方法(多个)
如果当前的主体有任何提供的角色(给定的作为一个逗号分隔符的字符串列表)的话,返回true。假设有个路径管理员可以访问,普通用户都能访问,则用这个方法设置。
2.1 配置类
.antMatchers("/test/index").hasAnyAuthority("admins,normal") //用户有admins或normal权限都能访问
2.2 UserDetailsService
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("normal");
3.hasRole(一个)
如果用户具备给定角色
就允许访问,否则403
3.1 配置类
.antMatchers("/test/index").hasRole("sale") //当前用户需要有这个角色才能操作
3.2 2.2UserDetailsService
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins,ROLE_sale");
4 hasAnyRole
和上述一样,只是在配置类中设置多个角色。
自定义403页面
在配置类里进行配置
http.exceptionHandling().accessDeniedPage("/unauth.html");
创建403页面unauth.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>没有访问的权限</h1>
</body>
</html>
Spring Security认证授权相关注解
1.@Secured
判断是否具有角色
,另外需要注意这里匹配的字符串需要加前缀"ROLE_
",使用注解前要先开启注解功能
启动类上加@EnableGlobalMethodSecurity(securedEnabled = true)
使用方法如下
@GetMapping("update")
@Secured({"ROLE_sale","ROLE_manager"})
public String update(){
return "hello update";
}
UserDetailsService
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins,ROLE_sale");
2.@PreAuthorize
进入方法前进行权限
验证或角色
验证
2.1启动类上加@EnableGlobalMethodSecurity(securedEnabled = true)
2.2 controller
@GetMapping("update")
//@Secured({"ROLE_sale","ROLE_manager"})
@PreAuthorize("hasAnyAuthority('admins')")
// @PreAuthorize("hasAnyRole()")
public String update(){
return "hello update";
}
2.3 UserDetailService
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
3.@PostAuthorize
适合在方法执行完之后进行校验,适合验证有返回值的权限。
4.@PostFilter
权限验证后对返回数据进行过滤
@GetMapping("getall")
@PostFilter("filterObject.username == 'admin1'")
public List<Users> getall(){
List<Users> list = new ArrayList<>();
list.add(new Users("admin1","123"));
list.add(new Users("admin2","123"));
return list;
}
5.@PreFilter
数据进入controller前对数据进行过滤
@GetMapping("getallPre")
@PostFilter("filterObject.id %2==0")
public List<Users> getallPre(@RequestBody List<Users> users){
users.forEach(t -> {
System.out.println(t.getUsername());
});
return users;
}
用户注销
1.在配置类中添加退出映射地址
http.logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll();
.defaultSuccessUrl("/success.html").permitAll()
success.html登录成功页面 用于退出
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录成功!
<a href="/logout">退出</a>
</body>
</html>
基于数据库的记住我 自动登录
自动登录的方式
1.Cookie :客户端机制,内容存在浏览器中
2.Spring Security安全框架机制的自动登录
2.1实现原理:
当第二次访问时,获取cookie中的信息,和数据库中的信息进行对比,如果查询到对应信息,认证成功,可以登录。
详细原理:
案例实现:
1.创建数据库表格
JdbcTokenRepositoryImpl默认会帮我们创建,其源码sql如下
create table persistent_logins (
username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null)
2.配置类
注入数据源,配置操作数据库对象
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//jdbcTokenRepository.setCreateTableOnStartup(true); //自动生成表 persistent_logins 表
return jdbcTokenRepository;
}
.and().rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60) //设置自动登录有效时长 单位秒
.userDetailsService(userDetailsService)
3.在登录页面添加复选框
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login" method="post">
用户名:<input type="text" name="username">
<br>
密码:<input type="password" name="password">
<br>
<input type="checkbox" name="remember-me">自动登录 <!--注意这里name必须是remember-me-->
<br>
<input type="submit" value="login">
</form>
</body>
</html>
CSRF (Spring Security4.0之后默认开启)
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
从Spring Security4.0之后默认开启,它会针对PATCH,POST,PUT,DELETE方法进行保护
实现原理
认证成功后生成csrfToken保存到HttpSession或者Cookie中。每次请求都会带着这个Token 值,利用这个Token值和session中的token值做比较,一样则允许访问
源码
1.CsrfFilter
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
//生成token存入session
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
final boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
//
request.setAttribute(CsrfToken.class.getName(), csrfToken);
//拿到表单传过来的token值
request.setAttribute(csrfToken.getParameterName(), csrfToken);
//判断和session中的token中是否一样
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for "
+ UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response,
new MissingCsrfTokenException(actualToken));
}
else {
this.accessDeniedHandler.handle(request, response,
new InvalidCsrfTokenException(csrfToken, actualToken));
}
return;
}
filterChain.doFilter(request, response);
}
实现案例:
在表单中增加隐藏项
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login" method="post">
<input type="hidden" name="${_csrf.parameterName}}" value="${_csrf.token}}">
用户名:<input type="text" name="username">
<br>
密码:<input type="password" name="password">
<br>
<input type="checkbox" name="remember-me">自动登录 <!---->
<br>
<input type="submit" value="login">
</form>
</body>
</html>
Spring Security微服务权限方案
微服务认证和授权过程