401JavaSpringSecurity5.7.5 GA

一、入门

认证:AuthenticationManager、Authentication、SecurityContextHolder

授权:AccessDecisionManager、AccessDecisionVoter、ConfigAtrribute

一、依赖

  • 继承父工程spring-boot-starter-parent(必要)
  • spring-boot-starter-web(必要)
  • spring-boot-starter-security(必要)
  • spring-boot-starter-test(可选)
  • spring-security-test(可选)
  • themyleaf(可选,后面要用到)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.miao</groupId>
    <artifactId>SpringSecurity-First-Start</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SpringSecurity-First-Start</name>
    <description>SpringSecurity-First-Start</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

二、起步

  • 用户名为user
  • 密码在日志中显示

1. Controller层

package com.miao.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/hello")
public class HelloSecurity {

    @RequestMapping("/world")
    public String test1() {
        return "<h1>Hello Spring Security!</h1>";
    }
}

2. 测试

  1. 访问http://localhost:8080/hello/world
  2. 出现http://localhost:8080/login登录
  3. http://localhost:8080/logout登出

三、认证(核心)

认证

  • 发起者AbstractAuthenticationProcessingFilter

  • AbstractUserDetailsAuthenticationProvider.preAuthenticationChecks第一次检查

  • AbstractUserDetailsAuthenticationProvider.postAuthenticationChecks第二次检查

前置、内存、数据库及自定义认证

  • 看源码时候注意On和Missing,若同时有这两个注解,如DefaultWebSecurityCondition.java,那么表示On里面的类都要存在,Missing实例不存在,默认配置才会生效。
1. 资源过滤器(看源码)
  • SecurityFilterChain
  1. Spring Security默认提供了过滤器(哪些页面需要认证/拦截,默认是全部都拦截/认证)。

  2. 请看DefaultWebSecurityCondition.java可知,有WebSecurityconfigureAdapter(弃用)或者SecurityFilterChain的Bean实例,那么这个默认提供的过滤器就会失效

  3. 需满足两个条件才会触发默认过滤配置,即有SecurityFilterChain.class, HttpSecurity.class两个类文件(注意:是文件就行)并且WebSecurityConfigurerAdapter.class,SecurityFilterChain.class两个Bean实例不存在。
    @ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })
    // 导入SpringSecurity就肯定有这两个类SecurityFilterChain.class, HttpSecurity.class的文件
    static class Classes {
    
    }
    
    @ConditionalOnMissingBean({
    // 如果想要自定义过滤,那么创建其中任意一个实例即可
        org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter.class,
          SecurityFilterChain.class })
    @SuppressWarnings("deprecation")
    
  4. 我们可以自定义过滤方式(如公共资源不需要认证,保护资源才要认证),通过创建WebSecurityconfigureAdapter(弃用)或者SecurityFilterChain的Bean实例

2. 用户认证(看源码)

number 1Servlet Authentication Architecture :: Spring Security

number 2Topical Guide | Spring Security Architecture

  1. Spring Security默认提供了内存用户认证:查看SecurityProperties.java(创建默认用户)和UserDetailsServiceAutoConfiguration.java(创建认证)。可以通过以下来设置内存用户认证
@Bean
public InMemoryUserDetailsManager userDetailsService() {
    PasswordEncoder passwordEncoder = passwordEncoder();
    UserDetails user = User.withUsername("root")
            .password(passwordEncoder.encode("123456"))
            .roles()
            .build();
        return new InMemoryUserDetailsManager(user);
    }
  1. 我们可以自定义用户认证那么我们只要使用了AuthenticationManager、AuthenticationProvider、UserDetailsService、AuthenticationManagerResovler(这四个系统都没有使用)那么这个SecurityProperties.java中默认提供的InMemoryUserDetailsManager实例(默认用户user不再生成/失效)就失效了,我们就自定义用户认证了,如数据库用户认证
3. 注解
  • @EnableWebSecurity:在非Springboot的Spring Web MVC应用中,该注解@EnableWebSecurity需要开发人员自己引入以启用Web安全。而在基于Springboot的Spring Web MVC应用中,开发人员没有必要再次引用该注解,Springboot的自动配置机制WebSecurityEnablerConfiguration已经引入了该注解。
  • @EnableGlobalMethodSecurity(prePostEnabled = true) :启用方法级别认证。为true时可以使用以下注解
    • @PreAuthorize
    • @PostAuthorize

4.1 HttpSecurity
  1. HttpSecurity仅用于定义需要安全控制的请求(当然HttpSecurity也可以指定某些请求不需要安全控制);
4.2 WebSecurity
  1. WebSecurity不仅通过HttpSecurity定义某些请求的安全控制,也通过其他方式定义其他某些请求可以忽略安全控制;
  2. 使用WebSecurity.ignoring()忽略某些URL请求,这些请求将被Spring Security忽略,这意味着这些URL将有受到 CSRF、XSS、Clickjacking 等攻击的可能

一、自定义用户认证(重要)

  • 最终用DaoAuthenticationProvider中的retrieveUser方法进行认证
一、自定义认证
  • 框架默认AuthenticationManagerBuilder(全局):

    • 检测到UserDetailsService的Bean实例后,会自动设置其为数据源。
    • 全局都可以@Autowire这个默认builder实例,即注入到需要用到它的地方。
    • 不需要继承WebSecurityConfigurerAdapter
    @Autowired
    private AuthenticationManagerBuilder authenticationManagerBuilder;
    
  • 框架默认AuthenticationManager(全局):

    • 不需要继承WebSecurityConfigurerAdapter
    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
    
  • 自定义AuthenticationManager(本地,可设置成全局):

    • 会覆盖默认的,即用默认的设置用户也无效,并且要用userDetailsService方法手动设置数据源。
    • 只能在重写的configure方法中使用这个实例,不能注入到其他地方。需要重写authenticationManagerBean方法并加上**@Bean**,才能生成一个自定义的全局AuthenticationManager实例。
    • 需要继承WebSecurityConfigurerAdapter
    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication().withUser("root").password("{noop}123").roles();
        }
    
        @Override
        @Bean
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    }
    
一、旧版
  • 法一需要继承WebSecurityConfigureAdapter
  • 法二不需要继承WebSecurityConfigureAdapter
package com.miao.config;

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.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// public class WebSecurityConfig {
    /*
        若使用了BCryptPasswordEncoder加密则必须使用!
        会要配置给框架,用来加密我们输入的密码,然后才能让它与内存中保存的密码校验上
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
     /* 
	 	法一辅助,若想使用这个UserDetailsService,法二builder必须使用useUserDeatails方法
	 	当然,这个可以不使用@Bean
	 */
    
     /*
        法二辅助,若想使用这个UserDetailsService,法一builder可不做任何其他操作
        1.自动:当存在这个UserDetailsService的Bean实例后,将会自动配置给框架默认的AuthenticationManagerBuilder来操作。
        2.手动:使用注入的默认的builder.userDetailsService
     */


//    @Bean  // 用自定义的可不要这个@Bean
//    public UserDetailsService userDetailsService() {
//        InMemoryUserDetailsManager userDetails = new InMemoryUserDetailsManager();
//        userDetails.createUser(User.withUsername("user").password(new BCryptPasswordEncoder().encode("12345")).roles("user").build());
//        return userDetails;
//    }
    
    
    /*
        法一: 继承WebSecurityConfigurerAdapter后,重写configure方法,进行自定义。
        当与法一同时存在,就会覆盖法一的作用,若要使用UserDetailsService必须使用userDetailsService方法
     */
//    @Override
//    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
//        builder.inMemoryAuthentication().withUser("root").password(new BCryptPasswordEncoder().encode("123")).roles();
//        builder.userDetailsService(userDetailsService());
//    }

    /*
       法二:springsecurity中默认会生成AuthenticationManagerBuilder的一个Bean实例,我们直接注入
     */
//    @Autowired
//    public void init(AuthenticationManagerBuilder builder) throws Exception {
//        // 基于内存,法一
//        builder.inMemoryAuthentication().withUser("root").password(new BCryptPasswordEncoder().encode("123")).roles();
//        // 基于内存,法二
//        InMemoryUserDetailsManager userDetails = new InMemoryUserDetailsManager();
//        userDetails.createUser(User.withUsername("admin").password(new BCryptPasswordEncoder().encode("1234")).roles("admin").build());
//        builder.userDetailsService(userDetails);  // 指定数据源
//    }
}

二、新版
  • 利用全局默认的AuthenticationManagerBuilder,自动接收UserDetails的实例对象。
package com.miao.provider;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class NewMyUserDetails implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return User.withUsername("newuser").password(new BCryptPasswordEncoder().encode("123456")).roles().build();
    }
}
package com.miao.config;

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.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class WebSecurityConfig {
    // 若使用了BCryptPasswordEncoder加密则必须使用!
    // 会要配置给框架,用来加密我们输入的密码,然后才能让它与内存中保存的密码校验上
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
二、基于内存用户认证(入门)
  • 继承WebSecurityConfigureAdapter(已经弃用2022.8.29)
  • 使用User.withUserName方法
一、 系统默认认证
  • 详情请看SecurityProperties.java的源码
  1. 框架自行使用默认的内存认证方式,生成用户名为user和在日志中呈现的密码。

  2. 框架默认的类会先检测配置文件是否有user用户的设置,若没有则自行使用默认的内存认证方式生成默认用户
    spring.security.user.name=root
    spring.security.user.password=123456
    spring.security.user.roles=normal,admin
    
二、 常规认证(最常规登录)
1. Controller层
package com.miao.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @RequestMapping("/hello")
    public String sayHello() {
        return "使用内存中的用户信息";
    }
}
2. Config层
  • 需要使用@EnableWebSecurity
package com.miao.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.SecurityBuilder;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.WebSecurityConfigurer;
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.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class MyWebSecurityConfig {
    // 旧方法,需要继承WebSecurityConfigurerAdapter
//    @Override
//    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        PasswordEncoder encoder = passwordEncoder();
//        auth.inMemoryAuthentication().withUser("root").password(encoder.encode("1234567")).roles();
//    }


    // 1.
    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        // 无法再使用,因为总是有{bcrypt}前缀,无法登录!
//        UserDetails user = User.withDefaultPasswordEncoder()
//                .username("root")
//                .password("123456")
//                .roles()
//                .build();
//        System.out.println(user.getPassword());

        // 法一,需要下面的return new BCryptPasswordEncoder();或者直接new一个
        PasswordEncoder passwordEncoder = passwordEncoder();
        UserDetails user = User.withUsername("root")
                .password(passwordEncoder.encode("123456"))
                .roles()
                .build();
        System.out.println(user.getPassword());

        // 法二
//        PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
//        String pwd = encoder.encode("123456");
//        System.out.println(pwd);
//        UserDetails user = User.withUsername("root")
//                .password(pwd.substring(8))
//                .password(passwordEncoder.encode("123456"))
//                .roles()
//                .build();
        return new InMemoryUserDetailsManager(user);
    }

    // 2. 必须对密码加密才能使用
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 创建PasswordEncoder实现类,实现类就是加密算法
        return new BCryptPasswordEncoder();
    }

//    @Bean
//    protected SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
//        httpSecurity.authorizeRequests()
//                .anyRequest().authenticated()
//                .and()
//                .formLogin()
//                .disable()
//                .csrf()
//                .disable();
//        return httpSecurity.build();
//    }

//    @Bean
//    protected WebSecurityCustomizer webSecurityCustomizer() {
//        return web -> web.ignoring().antMatchers("/login");
//    }
}
三、 角色认证(方法级别)
  • 包含了常规认证,可以视为更为严格的过滤。
1. Controller层
package com.miao.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @RequestMapping("/h1")
    @PreAuthorize("hasAnyRole('normal', 'admin')")
    public String helloNomalAndAdminUser() {
        return "Hello normal和admin用户可以访问!";
    }

    @RequestMapping("/h2")
    @PreAuthorize("hasAnyRole('admin')")
    public String helloAdminUser() {
        return "Hello 只有admin用户可以访问!";
    }


}
2. Config层
  • 需要使用**@EnableWebSecurity**、@EnableGlobalMethodSecurity(prePostEnabled = true)
package com.miao.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)  // 启用方法级别认证
public class MyWebSecurityConfig {

  	// 法一
    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        PasswordEncoder passwordEncoder = passwordEncoder();
        UserDetails user1 = User.withUsername("normal")
                .password(passwordEncoder.encode("123456"))
                .roles("normal")
                .build();
        System.out.println("user1: " + user1.getPassword());
        UserDetails user2 = User.withUsername("admin")
                .password(passwordEncoder.encode("1234567"))
                .roles("admin")
                .build();
        System.out.println("user2: " + user2.getPassword());
        return new InMemoryUserDetailsManager(user1, user2);
    }
  
    // 法二
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        PasswordEncoder encoder = new BCryptPasswordEncoder();
        UserDetails user = User.withUsername("user").password(encoder.encode("123")).roles("normal").build();
        System.out.println(user.getPassword());
        manager.createUser(user);
        return manager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
三、基于数据库用户认证(常用)
  • 实现接口类UserDetailsService,返回UserDetails对象即可。
1. Controller层
package com.example.springsecurityfourthdatabaseauth1.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    @RequestMapping("/h1")
    // 角色认证,需要配置类使用@EnableGlobalMethodSecurity(prePostEnabled = true)
    @PreAuthorize("hasAnyRole('normal', 'admin')")
    public String helloNormalAndAdminUser() {
        return "Hello normal和admin用户可以访问!";
    }

    @RequestMapping("/h2")
  	// 角色认证,需要配置类使用@EnableGlobalMethodSecurity(prePostEnabled = true)
    @PreAuthorize("hasAnyRole('admin')")  
    public String helloAdminUser() {
        return "Hello 只有admin用户可以访问!";
    }
}
2. Config层
package com.example.springsecurityfourthdatabaseauth1.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
// 在非Springboot的Spring Web MVC应用中,该注解@EnableWebSecurity需要开发人员自己引入以启用Web安全。
// 而在基于Springboot的Spring Web MVC应用中,开发人员没有必要再次引用该注解,Springboot的自动配置机制WebSecurityEnablerConfiguration已经引入了该注解。
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)  // 开启方法级别的角色认证
public class MyWebSecurityConfig {
    // 给容器提供BCryptPasswordEncoder加密器,用来密码给我们输入的密码加密,
    // 并且让后续的框架能够拿到数据库加密过的密码与这个加密器加密的我们输入的密码进行校验。
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
3. Provider层实现
package com.example.springsecurityfourthdatabaseauth1.provider;

import com.example.springsecurityfourthdatabaseauth1.dao.UserInfoDao;
import com.example.springsecurityfourthdatabaseauth1.dao.entity.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
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.Component;

@Component
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserInfoDao userInfoDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 先准备一个UserDetails的对象
        UserDetails userDetails = null;
        UserInfo userInfo;
        // 2. 如果用户名存在,那么就开始我们的表演
        // 注意:这次没有判断用户/密码是否为空的情况,以后补上
        if (username != null) {
            // 3. 在数据库中通过用户名查找并返回实体类型对象
            userInfo = userInfoDao.findByUsername(username);

            if (userInfo != null) {
                // 4. 创建一个认证用户:通过刚刚获取的实体类型实例将数据库的这个用户变成需要认证的用户
                userDetails = User.withUsername(userInfo.getUsername()).password(userInfo.getPassword()).roles(userInfo.getRole()).build();
            }
        }

        // 5. 返回UserDetails对象,框架会接管它并在后续进行自动校验
        return userDetails;
    }
}

二、自定义资源认证(重要)

一、资源访问过滤
1. Controller层
package com.miao.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @RequestMapping("/hello1")
    public String hello() {
        System.out.println("Hello Security1!");
        return "hello security1!";
    }

    @RequestMapping("/hello2")
    public String hello2() {
        System.out.println("hello security2!!!");
        return "hello security2!!!";
    }
}
package com.miao.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class IndexController {

    @RequestMapping("/index")
    public String index() {
        System.out.println("Hello Index!");
        return "Hello Index1!";
    }
}
2. config层
package com.miao.config;

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.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class WebSecurityConfigurer {
    // 法一:通过创建WebSecurityConfigurerAdapter的Bean实例
    // 需要继承WebSecurityConfigurerAdapter
//    @Override
//    protected void configure(HttpSecurity http) throws Exception {
//        http.authorizeHttpRequests()  // 开启请求的权限管理
//                .mvcMatchers("/index").permitAll()  // 匹配请求,对/index放行,需要放在所有认证请求之前!!!
//                .anyRequest().authenticated()  // 除了/index,其他都要进行认证
//                .and()
//                .formLogin();  // 通过form表单认证
//    }

    // 法二:新版,通过创建SecurityFilterChain的Bean实例
    // IDEA会报错,说HttpSecurity没有找到Bean,忽略,因为是Bug
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        System.out.println(http);
        return http.authorizeHttpRequests()
                .mvcMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .build();
    }

}
二、自定义登陆界面
  • 什么时候用:系统默认登陆界面无法满足我们的需求

  • 框架接管认证的四个要求:

    • form表单请求方法的method属性为POST请求
    • form表单请求路径的action属性为/login(可以在SecurityFilterChain中指定其他的,故可以改变。这里用/dologin)
    • input用户名输入框的属性name为username(可以在SecurityFilterChain中指定其他的,故可以改变)
    • input密码输入框的属性name为password
    <form action="/dologin" method="post">
      用户名:<input type="text" name="username"><br>
      密码:<input type="password" name="password">
      <input type="submit" value="登陆">
    </form>
    
  • 需要取消CSRF跨站请求保护

1. 登录页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/dologin" method="post">
  用户名:<input type="text" name="username"><br>
  密码:<input type="password" name="password">
  <input type="submit" value="登陆">
</form>
</body>
</html>
2. Controller层
package com.miao.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @RequestMapping("/index")
    public String index() {
        return "This is Index!";
    }

    @RequestMapping("/hello")
    public String hello() {
        System.out.println("Hello 欢迎从登陆界面来啊!");
        return "Hello 欢迎从登陆界面来啊";
    }
}
package com.miao.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController {
    @RequestMapping("/login")
    public String login() {
        return "login";
    }
}
3. config层(传统Web)
package com.miao.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class WebSecurityConfigurer {
    // 法一:需要继承WebSecurityConfigurerAdapter
//    @Override
//    protected void configure(HttpSecurity http) throws Exception {
//        http.authorizeHttpRequests()
//                .mvcMatchers("/login").permitAll()  // 1. 先放行login界面
//                .mvcMatchers("/index").permitAll()
//                .anyRequest().authenticated()
//                .and()
//                .formLogin()
//                .loginPage("/login")  // 2. 指定自定义登陆界面
//                .loginProcessingUrl("/dologin")  // 指定处理登陆请求的url,告知框架以后是/dologin请求那么就是登陆请求
//                .csrf().disable()  // 3. 禁止跨站请求保护;
//    }

        @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeHttpRequests()
                .mvcMatchers("/login").permitAll()  // 1. 先放行login请求
                .mvcMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")  // 2. 指定登陆界面
                .loginProcessingUrl("/dologin")  // 指定处理登陆请求的url,告知框架以后是/dologin请求那么就是登陆请求
//                .usernameParameter("uname")  // 自定义参数名称
//                .passwordParameter("paswd")
                // 二选一
                // 3.1 请求成功后跳转的路径:始终是forward跳转,地址栏不变
                // 无论在之前访问了何种需要认证的资源,认证成功后都会强制forward到/index
                .successForwardUrl("/index")
                // 3.2 请求成功后跳转的路径:redirect跳转,地址栏改变
                // 之前需要认证的资源优先级更高,认证成功后不会强制redirect到/index
                // 加上第二个参数true时,认证成功后会强制redirect到/index
                // .defaultSuccessUrl("/index", true)
                .and()
                .csrf().disable()  // 4. 禁止跨站请求保护
                .build();
    }
}
三、自定义登陆成功处理
  • 什么时候用:光是页面跳转无法满足我们的需求或者前后端分离项目
  • 自定义AuthenticationSuccessHandler实现:
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "登陆成功");
        result.put("status", 200);
        result.put("authentication", authentication);
        response.setContentType("application/json;charset=utf-8");
        String s = new ObjectMapper().writeValueAsString(request);
        System.out.println(s);
        response.getWriter().println(s);
    }
}
1. 登录页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/login" method="post">
  用户名:<input type="text" name="username"><br>
  密码:<input type="password" name="password">
  <input type="submit" value="登陆">
</form>
</body>
</html>
2. Controller层
package com.miao.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @RequestMapping("/index")
    public String index() {
        return "This is Index!";
    }

    @RequestMapping("/hello")
    public String hello() {
        System.out.println("Hello 欢迎从登陆界面来啊!");
        return "Hello 欢迎从登陆界面来啊";
    }
}
package com.miao.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController {
    @RequestMapping("/login")
    public String login() {
        return "login";
    }
}
3. Config层(前后端分离)
package com.miao.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "登陆成功");
        result.put("status", 200);
        result.put("authentication", authentication);
        response.setContentType("application/json;charset=utf-8");
        String s = new ObjectMapper().writeValueAsString(result);  // map集合转json
        System.out.println(s);
        response.getWriter().println(s);
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
        AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);
    }
}
package com.miao.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class WebSecurityConfigurer {
        @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeHttpRequests()
                .mvcMatchers("/login").permitAll()
                .mvcMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/login")
                .successHandler(new MyAuthenticationSuccessHandler())  // 前后端分离处理方案
                .and()
                .csrf().disable()
                .build();
    }
}
四、自定义登陆失败及处理
  • forward跳转:异常SPRING_SECURITY_LAST_EXCEPTION存在request域中

  • redirect跳转:异常SPRING_SECURITY_LAST_EXCEPTION存在session域中

  • 自定义AuthenticationFailureHandler实现

1. 登陆页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/login" method="post">
  用户名:<input type="text" name="username"><br>
  密码:<input type="password" name="password">
  <input type="submit" value="登陆">
</form>
</body>
</html>
2. Controller层
package com.miao.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @RequestMapping("/index")
    public String index() {
        return "This is Index!";
    }

    @RequestMapping("/hello")
    public String hello() {
        System.out.println("Hello 欢迎从登陆界面来啊!");
        return "Hello 欢迎从登陆界面来啊";
    }

    @RequestMapping("/failure")
    public String fail() {
        return "登陆失败了!";
    }
}
package com.miao.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController {
    @RequestMapping("/login")
    public String login() {
        return "login";
    }
}
3. Config层(传统Web/前后端分离)
package com.miao.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class WebSecurityConfigurer {
        @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeHttpRequests()
                .mvcMatchers("/login").permitAll()
                .mvcMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/login")
                .successHandler(new MyAuthenticationSuccessHandler())  // 成功前后端分离处理方案
                // .failureForwardUrl("/failure")  // 认证失败后,forward跳转
                // .failureUrl("/login")  // 认证失败后,默认redirect跳转
                .failureHandler(new MyAuthenticationFailureHandler())  // 失败前后端分离处理方案
                .and()
                .csrf().disable()
                .build();
    }
}
package com.miao.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "登陆成功");
        result.put("status", 200);
        result.put("authentication", authentication);
        response.setContentType("application/json;charset=utf-8");
        String s = new ObjectMapper().writeValueAsString(result);  // map集合转json
        System.out.println(s);
        response.getWriter().println(s);
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
        AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);
    }
}
package com.miao.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "登陆失败" + exception.getMessage());
        result.put("status", 500);
        response.setContentType("application/json;charset=UTF-8");
        String s = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(s);
    }
}
五、注销登录
  • 默认提供了注销登录/logout
  • 自定义LogoutSuccessHandler实现
1. 登陆页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/login" method="post">
  用户名:<input type="text" name="username"><br>
  密码:<input type="password" name="password">
  <input type="submit" value="登陆">
</form>
</body>
</html>
2. Controller层
package com.miao.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @RequestMapping("/index")
    public String index() {
        return "This is Index!";
    }

    @RequestMapping("/hello")
    public String hello() {
        System.out.println("Hello 欢迎从登陆界面来啊!");
        return "Hello 欢迎从登陆界面来啊";
    }

    @RequestMapping("/failure")
    public String fail() {
        return "登陆失败了!";
    }
}
package com.miao.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController {
    @RequestMapping("/login")
    public String login() {
        return "login";
    }
}
3. Config层(传统Web/前后端分离)
package com.miao.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;

@Configuration
public class WebSecurityConfigurer {
        @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeHttpRequests()
                .mvcMatchers("/login").permitAll()
                .mvcMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/login")
                .successHandler(new MyAuthenticationSuccessHandler())  // 成功前后端分离处理方案
                .failureHandler(new MyAuthenticationFailureHandler())  // 失败前后端分离处理方案
                .and()
                .logout()  // 1. 拿到注销登录的配置对象
                // .logoutUrl("/logout")  // 2.1 指定注销登录的url,默认是GEt方式
                // 2.2 指定注销登录的url,可以设置所需要请求方式
                .logoutRequestMatcher(new OrRequestMatcher(
                        new AntPathRequestMatcher("/logout", "GET"),
                        new AntPathRequestMatcher("/logout1", "GET"),
                        new AntPathRequestMatcher("/logout2", "POST")
                        ))
                .invalidateHttpSession(true)  // 默认是true 会话失效
                .clearAuthentication(true)  // 默认是true 清楚认证标记
                .logoutSuccessUrl("/index")  // 3.1 redirect指定登出后的页面
                // 3.2 注销前后端开发处理方案
                .logoutSuccessHandler(new MyLogoutSuccessHandler())
                .and()
                .csrf().disable()
                .build();
    }
}
package com.miao.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "登陆成功");
        result.put("status", 200);
        result.put("authentication", authentication);
        response.setContentType("application/json;charset=utf-8");
        String s = new ObjectMapper().writeValueAsString(result);  // map集合转json
        System.out.println(s);
        response.getWriter().println(s);
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
        AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);
    }
}
package com.miao.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "登陆失败" + exception.getMessage());
        result.put("status", 500);
        response.setContentType("application/json;charset=UTF-8");
        String s = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(s);
    }
}
package com.miao.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "注销成功,当前的认证对象为" + authentication);
        result.put("status", 200);
        response.setContentType("application/json;charset=UTF-8");
        String s = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(s);
    }
}

三、获取用户信息

  • 什么时候使用:需要在传统Web/前后端分离,展示或者响应到页面

  • 用户信息保存在SecurityContextHolder(默认子线程从中无法获取用户信息,模式为THREADLOCALS)

  • 多线程获取信息需要添加VM参数:-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL

Servlet Authentication Architecture :: Spring Security

1. 单线程获取
@RestController
public class UserController {
    @RequestMapping("/user")
    public String show() {
        // 从SecurityContextHolder中获取用户信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User user = (User) authentication.getPrincipal();
//        System.out.println("用户信息" + authentication.getPrincipal());
        System.out.println("用户信息" + user);
        // 在配置文件中设置了spring.security.user.roles=admin
        System.out.println("权限信息" + authentication.getAuthorities()
        );
        return "Hi User!";
    }
}
2. 多线程获取
  • 需添加VM参数**-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL**(该模式将父线程的数据复制一份来)
@RequestMapping("/user1")
public String show1() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    User user = (User) authentication.getPrincipal();
    System.out.println("用户信息" + user);
    System.out.println("权限信息" + authentication.getAuthorities());

    // 子线程
    new Thread(() -> {
        Authentication authentication1 = SecurityContextHolder.getContext().getAuthentication();
        System.out.println("这是子线程的信息" + authentication1);
    }).start();
    return "Hi User!";
}

四、*Web开发总结

id(biting)password(varchar255)role (varchar255)username(varchar255)
1$2a 10 10 10dpc3P4HfOUyFCI7slNn4wuCZ6suvcFcKAF9GcRGPMFqvx3PYdVYEWnormalRoot
2$2a 10 10 10y2sYiz8OTWNzuvQ7NU6Kh.kplLoZMJAoyCtETk0.DTSYYIUChuqwqadminAdmin
一、传统Web
  • 添加验证码,需要依赖Kaptcha,需要重写过滤方法,如MyVerifyCodeFilter
  • 使用的认证方法是默认的UserNamePasswordAuthenticationFilter
  • 需要使用AuthenticationManager
1. config层
package com.miao.config;

import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

@Configuration
public class KaptchaConfig {
    @Bean
    public Producer kaptcha() {
        Properties properties = new Properties();
        // 1. 验证码宽度
        properties.setProperty("kaptcha.image.width", "150");
        // 2. 验证码高度
        properties.setProperty("kaptcha.image.height", "50");
        // 3. 验证码字符串
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
        // 4. 验证码长度
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}
package com.miao.config;

import com.miao.filter.MyVerifyCodeFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Configuration
public class WebSecurityConfig {
    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public MyVerifyCodeFilter myVerifyCodeFilter() throws Exception {
        MyVerifyCodeFilter myVerifyCodeFilter = new MyVerifyCodeFilter();
        // 指定认证管理器
        myVerifyCodeFilter.setAuthenticationManager(authenticationManager());
        // 指定认证成功处理
        myVerifyCodeFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                response.sendRedirect("/index");
            }
        });
        // 指定认证失败处理
        myVerifyCodeFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                response.sendRedirect("/login");
            }
        });
        return myVerifyCodeFilter;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

//    @Bean
//    public UserDetailsService userDetailsService() {
//        InMemoryUserDetailsManager userDetails = new InMemoryUserDetailsManager();
//        userDetails.createUser(User.withUsername("root").password(new BCryptPasswordEncoder().encode("12345")).roles("admin").build());
//        return userDetails;
//    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .mvcMatchers("/login").permitAll()
                .mvcMatchers("/vc.jpg").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")  // 自定义登陆界面
                // .loginProcessingUrl("/login") 自定义处理login路径
                .defaultSuccessUrl("/index")
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login")
                .and()
                .addFilterAt(myVerifyCodeFilter(), UsernamePasswordAuthenticationFilter.class)  // 替换成验证码过滤器
                .csrf().disable()
                .build();

    }
}
2. controller层
package com.miao.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Controller
public class LoginController {

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

    @RequestMapping("/index")
    public String index() {
        return "index";
    }
}
package com.miao.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @RequestMapping("/user")
    public String getUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        System.out.println(authentication.getPrincipal());
        System.out.println(authentication.getAuthorities());
        return authentication.toString();
    }
}
package com.miao.controller;

import com.google.code.kaptcha.Producer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;
import java.io.IOException;

@Controller
public class VerifyController {
    @Autowired(required = true)
    private Producer producer;

    @RequestMapping("/vc.jpg")
    public void verifyCode(HttpServletResponse response, HttpSession session) throws IOException {
        // 1. 生成验证码
        String verifyCode = producer.createText();
        // 2. 保存在session中,日后可以放入redis中
        session.setAttribute("kaptcha", verifyCode);
        // 3. 生成图片
        BufferedImage bufferedImage = producer.createImage(verifyCode);
        // 4. 响应图片
        response.setContentType("image/jpg");
        ServletOutputStream os = response.getOutputStream();
        ImageIO.write(bufferedImage, "jpg", os);
    }
}
3. dao层
package com.miao.dao.mapper;

import com.miao.dao.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface UserMapper {
    User selectUserByUserName(@Param("username") String username);
}
package com.miao.dao.pojo;

import org.springframework.security.core.userdetails.UserDetails;

public class User {
    private Integer id;
    private String username;
    private String password;
    private String role;

    public User() {

    }

    public User(Integer id, String username, String password, String role) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.role = role;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer 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;
    }

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", role='" + role + '\'' +
                '}';
    }
}
4. exception层
package com.miao.exception;

import org.springframework.security.core.AuthenticationException;

// 自定义验证码异常
public class MyVerifyCodeException extends AuthenticationException {
    public MyVerifyCodeException(String msg, Throwable cause) {
        super(msg, cause);
    }

    public MyVerifyCodeException(String msg) {
        super(msg);
    }
}
5. filter层
package com.miao.filter;

import com.miao.exception.MyVerifyCodeException;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.ObjectUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

// 自定义验证码
public class MyVerifyCodeFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        // 1. 从请求中获取验证码
        String verifyCode = request.getParameter("verifycode");
        // 2. 与session中的验证码进行比较
        String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha");
        if (!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(sessionVerifyCode)) {
            verifyCode.equalsIgnoreCase(sessionVerifyCode);
            return super.attemptAuthentication(request, response);
        }
        throw new MyVerifyCodeException("验证码不匹配!");
    }
}
6. provider层
package com.miao.provider;

import com.miao.dao.mapper.UserMapper;
import com.miao.dao.pojo.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.Component;
import org.springframework.util.ObjectUtils;

@Component
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.selectUserByUserName(username);
        if (ObjectUtils.isEmpty(user)) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        UserDetails userDetails = org.springframework.security.core.userdetails.User
                .withUsername(user.getUsername())
                .password(user.getPassword())
                .roles(user.getRole())
                .build();
        return userDetails;
    }
}
7. 前端页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆界面</title>
</head>
<body>
<form action="/login" method="POST">
    用户名:<input type="text" name="username"><br>
    密码:&nbsp;&nbsp;<input type="password" name="password"><br>
    验证码:<input type="text" name="verifycode"> <img src="/vc.jpg" alt="验证码"><br>
    <input type="submit" value="登陆">
</form>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>系统主页</title>
</head>
<body>
<div>
  <h1>欢迎来到我的主页!</h1>
</div>
</body>
</html>
8. 配置文件
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://101.43.117.227:3308/SSM
    username: root
    password: 123456

mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  type-aliases-package: com.miao.dao.pojo
  mapper-locations: classpath:com/miao/dao/mapper/*Mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!--接口类全类名-->
<mapper namespace="com.miao.dao.mapper.UserMapper">
    <select id="selectUserByUserName" resultType="User">
        SELECT * FROM user_info WHERE username=#{username}
    </select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.miao</groupId>
    <artifactId>SpringSecurity-CustomRU1-Web1</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SpringSecurity-CustomRU1-Web1</name>
    <description>SpringSecurity-CustomRU1-Web1</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <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.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
二、前后端分离
  • 添加验证码,需要依赖Kaptcha
  • 需要重写认证方法,如MyLoginAuthenticationFilter
  • 需要使用AuthenticationManager
1. config层
package com.miao.config;

import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

@Configuration
public class KaptchaConfig {
    @Bean
    public Producer producer() {
        Properties properties = new Properties();
        // 1. 验证码宽度
        properties.setProperty("kaptcha.image.width", "150");
        // 2. 验证码高度
        properties.setProperty("kaptcha.image.height", "50");
        // 3. 验证码字符串
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
        // 4. 验证码长度
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}
package com.miao.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.miao.filter.MyLoginAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Configuration
//public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public class WebSecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    public MyLoginAuthenticationFilter myLoginAuthenticationFilter() throws Exception {
        MyLoginAuthenticationFilter myLoginAuthenticationFilter = new MyLoginAuthenticationFilter();
        // 可以用其父类的方法自定义表单input的name
        // myLoginAuthenticationFilter.setUsernameParameter("uname");
        // myLoginAuthenticationFilter.setUsernameParameter("pwd");

        // 旧版:
        // myLoginAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());  // 指定认证管理器
        // 新版
        myLoginAuthenticationFilter.setAuthenticationManager(authenticationManager());
        myLoginAuthenticationFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                Map<String, Object> result = new HashMap<>();
                result.put("msg", "登陆成功");
                result.put("status", 200);
                response.setStatus(HttpStatus.OK.value());
                result.put("用户信息", (User) (authentication.getPrincipal()));
                response.setContentType("application/json;charset=UTF-8");
                String s = new ObjectMapper().writeValueAsString(result);
                response.getWriter().println(s);
            }
        });  // 认证成功处理
        myLoginAuthenticationFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                Map<String, Object> result = new HashMap<>();
                result.put("msg", "登陆失败" + exception.getMessage());
                result.put("status", 500);
                response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
                response.setContentType("application/json;charset=UTF-8");
                String s = new ObjectMapper().writeValueAsString(result);
                response.getWriter().println(s);
            }
        });  // 认证失败处理
        return myLoginAuthenticationFilter;
    }


    // 新版
    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .mvcMatchers("/login").permitAll()
                .mvcMatchers("/vc.jpg").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/login")
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(new AuthenticationEntryPoint() {
                    @Override
                    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                        response.setContentType("application/json;charset=UTF-8");
                        response.setStatus(HttpStatus.UNAUTHORIZED.value());
                        response.getWriter().println("尚未认证,请先认证!");
                    }
                })  // 认证异常,会覆盖原有的loginPage,因为前后端没有所谓的登录页面
                .and()
                .addFilterAt(myLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)  // 将框架中默认的某个过滤器替换
                .logout()
                .logoutUrl("/logout")
//                .logoutSuccessUrl("/login")
                .logoutRequestMatcher(new OrRequestMatcher(
                        // 可用delete/get方式注销用户
                        new AntPathRequestMatcher("/logout", HttpMethod.DELETE.name()),
                        new AntPathRequestMatcher("/logout", HttpMethod.GET.name())
                ))
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        Map<String, Object> result = new HashMap<>();
                        result.put("msg", "注销成功");
                        result.put("用户信息", authentication.getPrincipal());
                        response.setContentType("application/json;charset=UTF-8");
                        response.setStatus(HttpStatus.OK.value());
                        String s = new ObjectMapper().writeValueAsString(result);
                        response.getWriter().println(s);
                    }
                })
                .and()
                .csrf().disable()
                .build();
    }

    /*

        旧版

     */
//    @Override
//    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        super.configure(auth);
//    }
//    @Override
//    @Bean
//    public AuthenticationManager authenticationManagerBean() throws Exception {
//        return super.authenticationManagerBean();
//    }
//    @Override
//    protected void configure(HttpSecurity http) throws Exception {
//        http.authorizeRequests()
//                .mvcMatchers("/login").permitAll()
//                .anyRequest().authenticated()
//                .and()
//                .formLogin()
//                .and()
//                .addFilterAt(myLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)  // 将框架中默认的某个过滤器替换
//                .csrf().disable();
//    }
}
2. controller层
package com.miao.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController {

    @RequestMapping("/login")
    public String login() {
        System.out.println();
        return "login";
    }

    @RequestMapping("/index")
    public String index() {
        return "index";
    }
}
package com.miao.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @RequestMapping("/user")
    public String getUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        System.out.println(authentication.getPrincipal());
        System.out.println(authentication.getAuthorities());
        return authentication.toString();
    }
}
package com.miao.controller;

import com.google.code.kaptcha.Producer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Base64Utils;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;
import java.io.IOException;

@RestController
public class VerifyController {
    @Autowired
    private Producer producer;

    @RequestMapping("/vc.jpg")
    public String verifyCode(HttpServletResponse response, HttpSession session) throws IOException {
        // 1. 生成验证码
        String verifyCode = producer.createText();
        // 2. 保存在session中,日后可以放入redis中
        session.setAttribute("kaptcha", verifyCode);
        System.out.println(verifyCode);
        // 3. 生成图片
        BufferedImage bufferedImage = producer.createImage(verifyCode);
        // 4. 响应图片,通过base64
        response.setContentType("image/jpg");
        FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
        ImageIO.write(bufferedImage, "jpg", fos);
        return Base64Utils.encodeToString(fos.toByteArray());
    }
}
3. dao层
package com.miao.dao.mapper;

import com.miao.dao.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface UserMapper {
    User selectUserByUserName(@Param("username") String username);
}
package com.miao.dao.pojo;

public class User {
    private Integer id;
    private String username;
    private String password;
    private String role;

    public User() {

    }

    public User(Integer id, String username, String password, String role) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.role = role;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer 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;
    }

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", role='" + role + '\'' +
                '}';
    }
}
4. exception层
package com.miao.exception;

import org.springframework.security.core.AuthenticationException;

// 自定义验证码异常
public class MyVerifyCodeException extends AuthenticationException {
    public MyVerifyCodeException(String msg, Throwable cause) {
        super(msg, cause);
    }

    public MyVerifyCodeException(String msg) {
        super(msg);
    }
}
5. filter层
package com.miao.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.miao.exception.MyVerifyCodeException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
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 org.springframework.util.ObjectUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

// 自定义认证方法
public class MyLoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 1. 判断是否是post方式请求,注意有!号
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        // .1 获取请求数据
        try {
            Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
            String kaptcha = userInfo.get("kaptcha");
            String username = userInfo.get(getUsernameParameter());
            String password = userInfo.get(getPasswordParameter());

            // .2 获取session/redis中的验证码
            String sessionverifyCode = (String) request.getSession().getAttribute("kaptcha");
            System.out.println(sessionverifyCode);
            if (!ObjectUtils.isEmpty(kaptcha) && !ObjectUtils.isEmpty(sessionverifyCode)) {
                kaptcha.equalsIgnoreCase(sessionverifyCode);
                // .3 获取用户名和密码认证
                UsernamePasswordAuthenticationToken authRequset =  new UsernamePasswordAuthenticationToken(username, password);
                setDetails(request, authRequset);
                return this.getAuthenticationManager().authenticate(authRequset);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        throw new MyVerifyCodeException("验证码不匹配");

        /*
            以下为没有验证码功能的单纯的 用户名密码验证逻辑

         */
//        // 2. 判断是否是json格式数据
//        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
//            // 3. 从json数据中获取用户输入的用户名和密码
//            try {
//                Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
//                String username = userInfo.get(getUsernameParameter());
//                String password = userInfo.get(getPasswordParameter());
//                System.out.println("用户名是" + username + "  密码是" + password);
//
//                UsernamePasswordAuthenticationToken authRequset =  new UsernamePasswordAuthenticationToken(username, password);
//                setDetails(request, authRequset);
//                return this.getAuthenticationManager().authenticate(authRequset);
//            } catch (IOException e) {
//                e.printStackTrace();
//            }
//        }
//        return super.attemptAuthentication(request, response);
    }
}
6. provider层
package com.miao.provider;

import com.miao.dao.mapper.UserMapper;
import com.miao.dao.pojo.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.Component;
import org.springframework.util.ObjectUtils;

@Component
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.selectUserByUserName(username);
        if (ObjectUtils.isEmpty(user)) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        UserDetails userDetails = org.springframework.security.core.userdetails.User
                .withUsername(user.getUsername())
                .password(user.getPassword())
                .roles(user.getRole())
                .build();
        return userDetails;
    }
}
7. 前端页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆界面</title>
</head>
<body>
<form action="/login" method="POST">
    用户名:<input type="text" name="username"><br>
    密码:&nbsp;&nbsp;<input type="password" name="password"><br>
    验证码:<input type="text" name="verifycode"> <img src="data:image/png;base64,base64的代码" alt="验证码"><br>
    <input type="submit" value="登陆">
</form>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>系统主页</title>
</head>
<body>
<div>
  <h1>欢迎来到我的主页!</h1>
</div>
</body>
</html>
8. 配置文件
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://101.43.117.227:3308/SSM
    username: root
    password: 123456

mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  type-aliases-package: com.miao.dao.pojo
  mapper-locations: classpath:com/miao/dao/mapper/*Mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.miao</groupId>
    <artifactId>SpringSecurity-CustomRU1-Web2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SpringSecurity-CustomRU1-Web2</name>
    <description>SpringSecurity-CustomRU1-Web2</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.github.penggle/kaptcha -->
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

五、关闭认证

  1. 启动类设置
  • 排除SpringSecurity配置**@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})**即可。
package com.miao;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;

@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
public class SpringSecurityFirstHelloSecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityFirstHelloSecurityApplication.class, args);
    }

}

四、密码加密

一、默认自定义加密

  • 使用框架提供的DelegatingPasswordEncoder进行加密的判断
  • 会根据{}中的内容来配置对应的加密类来判断
@Configuration
public class WebSecurityConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager memoryUserDetailsManager = new InMemoryUserDetailsManager();
        memoryUserDetailsManager.createUser(User
                                            .withUsername("root")
                                            .password("{noop}12345")  // 这是无加密的12345
                                            // .password("{bcrypt}$2a$10$c4D9s4BijkpQi2Pu4LQTPOLyHDTxQ.jj0I8i8C0c6HwcfbgivsUI6")  // 这是Bcrypt加密12345
                                            .roles()
                                            .build());
        return memoryUserDetailsManager;
    }
}

二、明确自定义加密

  • AuthenticationConfiguration中的LazyPasswordEncoder
// 法二:明确指定
@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

@Bean
public UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager memoryUserDetailsManager = new InMemoryUserDetailsManager();
    memoryUserDetailsManager.createUser(User
            .withUsername("root")
            .password("$2a$10$c4D9s4BijkpQi2Pu4LQTPOLyHDTxQ.jj0I8i8C0c6HwcfbgivsUI6")  // 不需要再加前缀,因为已经指定了加密方式
            .roles()
            .build());
    return memoryUserDetailsManager;
}

三、密码自动升级

  • DaoAuthenticationProvider.createSuccessAuthentication来升级,需要实现UserDetailsService和UserDetailsPasswordService

  • 默认使用DelegatingPasswordEncoder并且是BCryptPasswordEncoder来升级加密。

@Service
public class MyUserDetailsService implements UserDetailsService, UserDetailsPasswordService {
    @Autowired
    UserDao userDao;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        xxxxx逻辑
        return null;
    }
    
    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        // 1. 更新数据库中的垃圾密码
        // UPDATE xxx SET password=#{newPassword} WHERE username=#{username}
        Integer result = userDao.updatePwd(user.getUsername(), newPassword);
        // 2. 判断是否成功更新
        if (result == 1) {
            // 3. User是我们自己定义的实体类,实现了框架的User
            ((User) user).setpassword(newPassword);
        }
        return user;
    }
}

五、RemeberMe(自动登录)

一、默认RememberMe

  • RememberMeAuthenticationFilter
  • TokenBasedRememberMeServices
1. config层
package com.miao.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class WebSecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(User
                .withUsername("root")
                .password(new BCryptPasswordEncoder().encode("12345"))
                .roles("admin")
                .build());
        return userDetailsManager;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .rememberMe()  // 开启记住我
                // .alwaysRemember(true)  // 总是记住我,即无论是否勾选,都算作记住我
                // .rememberMeParameter("remember")  // 自定义input的参数名
                .and()
                .csrf().disable()
                .build();
    }
}
2. controller层
package com.miao.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String hello() {
        return "Hello Security!";
    }
}
3. 配置文件
# 设置身份过期时间为1分钟,勾选了记住我后则失效
server.servlet.session.timeout=1

二、提高安全性

  • PersistentTokenBasedRememberMeServices

  • 每次登录成功都会生成新的token和date

  • 必须导入,因为数据库令牌持久化需要,内存令牌持久化也需要

    • <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-jdbc</artifactId>
      </dependency>
      <dependency>
         <groupId>mysql</groupId>
         <artifactId>mysql-connector-java</artifactId>
      </dependency>
      
一. 内存令牌持久化
1. config层
package com.miao.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;

import java.util.UUID;

@Configuration
public class WebSecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService inMemoryUserDetailsManager() {
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(User
                .withUsername("root")
                .password(new BCryptPasswordEncoder().encode("12345"))
                .roles("admin")
                .build());
        return userDetailsManager;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeRequests().anyRequest().authenticated()
                // .mvcMatchers("/index").rememberMe()  // 指定资源记住我
                .and()
                .formLogin()
                .and()
                .rememberMe()  // 开启记住我
                .rememberMeServices(rememberMeServices())
                // .alwaysRemember(true)  // 总是记住我
                // .rememberMeParameter("remember")  // 自定义input的参数名
                // .rememberMeServices(rememberMeServices())  // 自定义rememberservice的实现
                .and()
                .csrf().disable()
                .build();
    }

    @Bean  // 必须加Bean否则无效
    public RememberMeServices rememberMeServices() {
        // 使用内存令牌实现
        return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), inMemoryUserDetailsManager(), new InMemoryTokenRepositoryImpl());
    }
}
    
二. 数据库令牌持久化
1. config层
package com.miao.config;

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.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;
import java.util.UUID;

@Configuration
public class WebSecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService inMemoryUserDetailsManager() {
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(User
                .withUsername("root")
                .password(new BCryptPasswordEncoder().encode("12345"))
                .roles("admin")
                .build());
        return userDetailsManager;
    }

    // 1. 基于内存令牌持久化
//    public RememberMeServices rememberMeServices() {
//        return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), inMemoryUserDetailsManager(), new InMemoryTokenRepositoryImpl());
//    }

    // 2. 基于数据库令牌持久化:
    @Autowired
    private DataSource dataSource;

    // 法一:需要手动创建表结构
//    public RememberMeServices rememberMeServices() {
//        // 手动创建表
//        /*
//        create table persistent_logins (
//        username varchar(64) not null, series varchar(64) primary key,
//        token varchar(64) not null, last_used timestamp not null
//        )
//         */
//        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
//        jdbcTokenRepository.setDataSource(dataSource);
//        return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), inMemoryUserDetailsManager(), jdbcTokenRepository);
//    }

    // 法二:自动创建表结构
    @Bean
    public PersistentTokenRepository rememberMeServices() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        jdbcTokenRepository.setCreateTableOnStartup(false);  // 第一次启动时改为true自动创建数据库表,后面又需要改成false
        return jdbcTokenRepository;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeRequests().anyRequest().authenticated()
                // .mvcMatchers("/index").rememberMe()  // 指定资源记住我
                .and()
                .formLogin()
                .and()
                .rememberMe()  // 开启记住我
                // .rememberMeServices(rememberMeServices())  // 数据库持久化法一
                .tokenRepository(new JdbcTokenRepositoryImpl())  // 数据库持久化法二
                // .alwaysRemember(true)  // 总是记住我
                // .rememberMeParameter("remember")  // 自定义input的参数名
                // .rememberMeServices(rememberMeServices())  // 自定义rememberservice的实现
                .and()java
                .csrf().disable()
                .build();
    }
}
2. 配置文件
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://101.43.117.227:3308/SSM
spring.datasource.username=root
spring.datasource.password=123456

三、自定义RememberMe

一、传统Web
1. config层
package com.miao.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class WebSecurityConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}12345").roles().build());
        return inMemoryUserDetailsManager;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .mvcMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/index", true)
                .and()
                .rememberMe()
                .and()
                .csrf().disable()
                .build();
    }
}
2. controller层
package com.miao.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class LoginController {
    @GetMapping("/login")
    public String login() {
        return "login.html";
    }

    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        return "Hello!";
    }

    @RequestMapping("/index")
    @ResponseBody
    public String index() {
        return "成功登录!这是主页!";
    }
}
3. 前端页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<form action="/login" method="post">
    用户名:<input type="text" name="username"><br>
    密码:<input type="password" name="password"><br>
    自动登录<input type="checkbox" name="remember-me"><br>
    <input type="submit" value="登录">
</form>
</body>
</html>
4. 配置文件
server.servlet.session.timeout=1
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://101.43.117.227:3308/SSM
spring.datasource.username=root
spring.datasource.password=123456
二、前后端分离
1. config层
package com.miao.config;

import org.springframework.core.log.LogMessage;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.servlet.http.HttpServletRequest;

public class MyRememerMe extends PersistentTokenBasedRememberMeServices {
    public MyRememerMe(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
        super(key, userDetailsService, tokenRepository);
    }

    @Override
    protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
        String rememberMe = request.getAttribute(parameter).toString();
        if (rememberMe != null) {
            if (rememberMe.equalsIgnoreCase("true") || rememberMe.equalsIgnoreCase("on")
                    || rememberMe.equalsIgnoreCase("yes") || rememberMe.equals("1")) {
                return true;
            }
        }
        this.logger.debug(
                LogMessage.format("Did not send remember-me cookie (principal did not set parameter '%s')", parameter));
        return super.rememberMeRequested(request, parameter);
    }
}
package com.miao.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.miao.filter.LoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@Configuration
public class WebSecurityConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}12345").roles().build());
        return inMemoryUserDetailsManager;
    }

    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;

    public LoginFilter loginFilter() throws Exception {
        LoginFilter filter = new LoginFilter();
        filter.setFilterProcessesUrl("/login");
        filter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
        filter.setRememberMeServices(rememberMeServices());  // 设置勾选了记住我时使用的自定义rememberme
        filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                Map<String, Object> result = new HashMap<>();
                result.put("msg", "登陆成功!");
                result.put("登陆成功用户信息", authentication.getPrincipal());
                response.setContentType("application/json;charset=UTF-8");
                response.setStatus(200);
                String s = new ObjectMapper().writeValueAsString(result);
                response.getWriter().println(s);
            }
        });
        filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                Map<String, Object> result = new HashMap<>();
                result.put("msg", "登陆失败!" + exception.getMessage());
                response.setContentType("application/json;charset=UTF-8");
                response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
                String s = new ObjectMapper().writeValueAsString(result);
                response.getWriter().println(s);
            }
        });
        return filter;
    }


    @Bean
    public RememberMeServices rememberMeServices() {
        return new MyRememerMe(UUID.randomUUID().toString(), userDetailsService(), new InMemoryTokenRepositoryImpl());
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .mvcMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .rememberMe()  // 开启记住我
                .rememberMeServices(rememberMeServices())  // 设置以后我们在配置文件中1分钟过期后,自动登录时使用的rememberme
                .and()
                .exceptionHandling()  // 异常处理
                .authenticationEntryPoint(new AuthenticationEntryPoint() {
                    @Override
                    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                        response.setContentType("application/json;charset=UTF-8");
                        response.setStatus(HttpStatus.UNAUTHORIZED.value());
                        response.getWriter().println("尚未认证,请先认证!");
                    }
                })
                .and()
                .logout()
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        Map<String, Object> result = new HashMap<>();
                        result.put("msg", "注销成功!");
                        result.put("注销成功用户信息", authentication.getPrincipal());
                        response.setContentType("application/json;charset=UTF-8");
                        response.setStatus(200);
                        String s = new ObjectMapper().writeValueAsString(result);
                        response.getWriter().println(s);
                    }
                })
                .and()
                .addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class)
                .csrf().disable()
                .build();
    }
}
2. controller层
package com.miao.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class LoginController {
    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        return "Hello!";
    }

    @RequestMapping("/index")
    @ResponseBody
    public String index() {
        return "成功登录!这是主页!";
    }
}
3. filter层
package com.miao.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
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 org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import org.springframework.util.ObjectUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 1. 判断是否为post请求
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("认证方法不支持" + request.getMethod());
        }

        // 2.1 判断是否为json格式请求数据
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
            // 3. 从json数据中获取用户名和密码
            try {
                Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String username = userInfo.get("username");
                String password = userInfo.get("password");
                String rememberMe = userInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER);
                // 4. 设置rememeberme键值对,给自定义Remember类使用
                if (!ObjectUtils.isEmpty(rememberMe)) {
                    request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER, rememberMe);
                }
                System.out.println("用户名为:" + username + "  密码为:" + password + "  remember-me为:" + rememberMe);
                // 5. 打包,交给认证管理器认证
                UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
                setDetails(request, token);
                return super.getAuthenticationManager().authenticate(token);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // 2.2 若不是json格式的数据,就尝试用父类的表单方式认证
        return super.attemptAuthentication(request, response);
    }
}
4. 配置文件
server.servlet.session.timeout=1
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://101.43.117.227:3308/SSM
spring.datasource.username=root
spring.datasource.password=123456

六、会话管理

一、管理登录(默认)

package com.miao.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.session.HttpSessionEventPublisher;

@Configuration
public class WebSecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable()
                .sessionManagement()  // 开启会话管理
                .maximumSessions(1) // 设置最大同时登录数量
                .and()
                .and().build();
    }

    // 可选的,用来监听会话管理
    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }
}

登录两个账号,其中一个会话会失效!

二、会话失效处理

1. 传统Web
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.authorizeRequests()
            .mvcMatchers("/index").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .csrf().disable()
            .sessionManagement()  // 开启会话管理
            .maximumSessions(1) // 最大同时登录数量
            .expiredUrl("/index")  // 指定会话失效的跳转页面
            .and()
            .and().build();
}
2. 前后端分离
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .csrf().disable()
            .sessionManagement()  // 开启会话管理
            .maximumSessions(1) // 最大同时登录数量
            .expiredSessionStrategy(new SessionInformationExpiredStrategy() {
                @Override
                public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
                    HttpServletResponse response = event.getResponse();
                    Map<String, Object> result = new HashMap<>();
                    result.put("status", 500);
                    result.put("msg", "当前会话已失效!请重新登录!");
                    String s = new ObjectMapper().writeValueAsString(result);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(s);
                }
            })
            .and()
            .and().build();
}

三、禁止再次登录

  • 默认是挤下线,当达到最大同时登录数量时,新设备可以继续登录,老的设备会被挤下线
  • 可以实现登录后,当达到最大同时登陆数量,新的设备必须等其中一个或多个退出登录后才能登录
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .csrf().disable()
            .sessionManagement()  // 开启会话管理
            .maximumSessions(1) // 最大同时登录数量
            .expiredSessionStrategy(new SessionInformationExpiredStrategy() {
                @Override
                public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
                    HttpServletResponse response = event.getResponse();
                    Map<String, Object> result = new HashMap<>();
                    result.put("status", 500);
                    result.put("msg", "当前会话已失效!请重新登录!");
                    String s = new ObjectMapper().writeValueAsString(result);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(s);
                }
            }).maxSessionsPreventsLogin(true)  // 禁止再次登录
            .and()
            .and().build();
}

四、会话共享

  • 在传统单机服务器中,以上会话管理没问题,因为会话管理的数据存在内存中

  • 在分布式,如集群中则失效,因为内存数据不同,所以需要会话共享,需要借助redis来解决

  • 需要依赖spring-boot-starter-data-redis和spring-session-data-redis

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .csrf().disable()
            .sessionManagement()  // 开启会话管理
            .maximumSessions(1) // 最大同时登录数量
            .expiredSessionStrategy(new SessionInformationExpiredStrategy() {
                @Override
                public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
                    HttpServletResponse response = event.getResponse();
                    Map<String, Object> result = new HashMap<>();
                    result.put("status", 500);
                    result.put("msg", "当前会话已失效!请重新登录!");
                    String s = new ObjectMapper().writeValueAsString(result);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(s);
                    response.flushBuffer();
                }
            }).maxSessionsPreventsLogin(true)  // 禁止再次登录
            .sessionRegistry(sessionRegistry())  // 使用redis的方案来管理会话
            .and()
            .and().build();
}

// 创建session同步到redis中
@Autowired
FindByIndexNameSessionRepository findByIndexNameSessionRepository;

@Bean
public SpringSessionBackedSessionRegistry sessionRegistry() {
    return new SpringSessionBackedSessionRegistry(findByIndexNameSessionRepository);
}
spring.redis.host=192.168.101.51
spring.redis.port=6380

七、CSRF漏洞保护

一、防御配置

  • 令牌同步模式,我们所有的带有请求的html页面,会被csrf过滤器自动加上令牌,也可以手动加上(在request.csrf.token)
.csrf()  // 默认也是开启的

二、CSRF保护使用

1. 传统Web
  • 令牌由服务器session保存
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .csrf()  // 开启防御
            .and()
            .build();
}
2. 前后端分离
  • 令牌由前端cookie保存

  • 需要header中带有特定的key和value,如X-XSRF-TOKEN 78bf9eff-816e-41d9-abc1-120d6dc89658

  • CsrfFilter中的doFilterInternal调试

1. config层
package com.miao.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.miao.filter.MyLoginAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class WebSecurityConfig {

    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    public MyLoginAuthenticationFilter myLoginAuthenticationFilter() throws Exception {
        MyLoginAuthenticationFilter myLoginAuthenticationFilter = new MyLoginAuthenticationFilter();
        myLoginAuthenticationFilter.setAuthenticationManager(authenticationManager());
        myLoginAuthenticationFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                Map<String, Object> result = new HashMap<>();
                result.put("msg", "登陆成功");
                result.put("status", 200);
                response.setStatus(HttpStatus.OK.value());
                result.put("用户信息", (User) (authentication.getPrincipal()));
                response.setContentType("application/json;charset=UTF-8");
                String s = new ObjectMapper().writeValueAsString(result);
                response.getWriter().println(s);
            }
        });  // 认证成功处理
        myLoginAuthenticationFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                Map<String, Object> result = new HashMap<>();
                result.put("msg", "登陆失败" + exception.getMessage());
                result.put("status", 500);
                response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
                response.setContentType("application/json;charset=UTF-8");
                String s = new ObjectMapper().writeValueAsString(result);
                response.getWriter().println(s);
            }
        });  // 认证失败处理
        return myLoginAuthenticationFilter;
    }


    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles().build());
        return userDetailsManager;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf()  // 开启防御,令牌存在于cookie中
                // 前后端令牌存储机制,将令牌保存到cookie中,并且使cookie能够让前端获取到
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .and()
                // 替换自定义过滤器
                .addFilterAt(myLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
//                .csrf().disable()  // 关闭防御,令牌存在于session中
                .build();
    }
}
2. controller层
package com.miao.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello() {
        return "Hello!";
    }
}
3. filter层
package com.miao.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
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 org.springframework.util.ObjectUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

// 自定义认证方法
public class MyLoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 1. 判断是否是post方式请求,注意有!号
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        // 2. 判断是否是json格式数据
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
            // 3. 从json数据中获取用户输入的用户名和密码
            try {
                Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String username = userInfo.get(getUsernameParameter());
                String password = userInfo.get(getPasswordParameter());
                System.out.println("用户名是" + username + "  密码是" + password);

                UsernamePasswordAuthenticationToken authRequset =  new UsernamePasswordAuthenticationToken(username, password);
                setDetails(request, authRequset);
                return this.getAuthenticationManager().authenticate(authRequset);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return super.attemptAuthentication(request, response);
    }
}
4. 请求数据

请求头:X-XSRF-TOKEN 78bf9eff-816e-41d9-abc1-120d6dc89658

json:

{
    "username": "root",
    "password": "123",
}

八、跨域

  • 请求页面
用来测试跨域 发起跨域请求

一、Spring解决方案

  • 不引入spring security依赖
1. @CrossOrigin
  • 局部有效
package com.miao.controller;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
// @CrosOrigin允许该类中所有方法的任何url都可以跨域请求
public class HelloController {

    @RequestMapping("/hello")
    // @CrossOrigin(origins = {"http://localhost:63342"})  // 解决跨域,填入能够发起跨域请求的url
    @CrossOrigin  // 允许所有url跨域请求
    public String hello() {
        return "hello!";
    }
}

二、Spring MVC解决方案

  • 不引入spring security依赖
1. addCorsMapping
  • 全局有效
package com.miao.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

// 自定义mvc配置类
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")  // 允许类中的哪些接口可以被跨域请求
                .allowCredentials(false)  // 不需要请求凭证
                .allowedMethods("*")  // 允许哪些方法(GET、POST等)
                .allowedOrigins("*")  // 允许哪些源
                .allowedHeaders("*")  // 允许哪些头
                .maxAge(3600);  // 预检请求最大时间

    }
}

三、Spring Web解决方案

1. CrosFilter
  • 全局有效
package com.miao.filter;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.lang.reflect.Array;
import java.util.Arrays;

@Configuration
public class WebCorsFilter {
    @Bean
    FilterRegistrationBean<CorsFilter> corsFilterFilterRegistrationBean() {
        FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedMethods(Arrays.asList("*"));
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
        corsConfiguration.setMaxAge(1800L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        registrationBean.setFilter(new CorsFilter(source));
        registrationBean.setOrder(-1);  // 设置优先级

        return registrationBean;
    }
}

四、Spring Security解决方案

  • Spring、Spring MVC方案都失效,Spring Web方案需要设置优先级(默认Spring Security过滤器优先级更高)

  • 需要先登录才能进行跨域请求(只能用于前后端分离)

package com.miao.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

@Configuration
public class WebSecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .cors().configurationSource(corsConfiguration())  // 开启跨域
                .and()
                .csrf().disable()
                .build();
    }

    public CorsConfigurationSource corsConfiguration() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedMethods(Arrays.asList("*"));
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
        corsConfiguration.setMaxAge(1800L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }

}

注意:点击发送跨域请求,返回的是需要登录的界面,但是已经没有报跨域错误。


九、异常处理

  • 主要解决认证和授权异常,其他异常交给mvc处理
  • AuthenticationException异常
  • AccessDeniedException异常

1. config层

package com.miao.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Configuration
public class WebSecurityConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("ADMIN").build());
        return userDetailsManager;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeRequests()
                .mvcMatchers("/login").permitAll()
                .mvcMatchers("/hello").hasRole("ADMIN")  // 需要ADMIN权限,否则发生授权异常
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(new AuthenticationEntryPoint() {
                    @Override
                    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                        response.setContentType("text/plain;charset=UTF-8");
                        response.setStatus(HttpStatus.UNAUTHORIZED.value());
                        response.getWriter().write("尚未认证!请先认证!");
                        response.sendRedirect("/login");  // 重定向至自定义登录页面
                    }
                })
                .accessDeniedHandler(new AccessDeniedHandler() {
                    @Override
                    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                        response.setContentType("text/plain;charset=UTF-8");
                        response.setStatus(HttpStatus.FORBIDDEN.value());
                        response.getWriter().write("无权访问");
                    }
                })
                .and()
                .csrf().disable()
                .build();
    }
}

2. controller层

package com.miao.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@Controller
public class HelloController {
    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        return "hello exception";
    }

    @RequestMapping("/login")
    public String index() {
        return "login.html";
    }
}

3. 自定义登录页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/login" method="post">
    <input type="text" name="username">
    <input type="password" name="password">
    <input type="submit">
</form>
</body>
</html>

十、授权(核心)

角色、权限

一、基于过滤器(URL)权限管理

  • FilterSecurityInterceptor,只能在请求前处理
1. config层
package com.miao.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class WebSecurityConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        // 注意这里是角色
        userDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("ADMIN", "USER").build());
        userDetailsManager.createUser(User.withUsername("user").password("{noop}123").roles("USER").build());
        // 注意这里是权限
        userDetailsManager.createUser(User.withUsername("win7").password("{noop}123").authorities("READ_INFO").build());
        return userDetailsManager;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeRequests()
                // 1. 进行授权,可以指定请求方式
                .mvcMatchers(HttpMethod.GET, "/admin").hasRole("ADMIN")  // 框架会自动加上ROLE_ADMIN
                .mvcMatchers("/user").hasRole("USER")  // 同上
                .mvcMatchers("/getInfo").hasAuthority("READ_INFO")  // 不会自动加
                // .antMatchers()  也能够实现,但mvc更强大
                // .regexMatchers()  也能够实现,但mvc更强大
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable()
                .build();
    }
}
2. controller层
package com.miao.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @RequestMapping("/admin")  // 必须要有ROLE_ADMIN角色
    public String admin() {
        return "admin ok";
    }

    @RequestMapping("/user")  // 必须要有ROLE_USER角色
    public String user() {
        return "user ok";
    }

    @RequestMapping("/getInfo")  // 必须要有READ_INFO权限
    public String getInfo() {
        return "info ok";
    }
}

二、基于AOP(方法)权限管理

  • MethodSecurityInterceptor,可以在请求前后处理
  • @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
1. config层
package com.miao.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
// 使权限相关注解有效
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true,  jsr250Enabled = true)
public class WebSecurityConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        // 注意这里是角色
        userDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("ADMIN", "USER").build());
        userDetailsManager.createUser(User.withUsername("user").password("{noop}123").roles("USER").build());
        // 注意这里是权限
        userDetailsManager.createUser(User.withUsername("win7").password("{noop}123").authorities("READ_INFO").build());
        return userDetailsManager;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeRequests()
                // 1. 进行授权,可以指定请求方式
                .mvcMatchers(HttpMethod.GET, "/admin").hasRole("ADMIN")  // 框架会自动加上ROLE_ADMIN
                .mvcMatchers("/user").hasRole("USER")  // 同上
                .mvcMatchers("/getInfo").hasAuthority("READ_INFO")  // 不会自动加
                // .antMatchers()  也能够实现,但mvc更强大
                // .regexMatchers()  也能够实现,但mvc更强大
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable()
                .build();
    }
}
2. controller层
package com.miao.controller;

import com.miao.entity.User;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.web.bind.annotation.*;

import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/hello")
public class AUthorizeMethodController {

    // 是ADMIN角色并且用户名是root才能访问
    @PreAuthorize("hasRole('ADMIN') and authentication.name=='root'")
    // 需要有READ_INFO权限
    // @PreAuthorize("hasAuthority('READ_INFO')")
    @GetMapping
    public String hello() {
        return "hello";
    }

    // 当传入的参数与用户名相等才能访问
    @PreAuthorize("authentication.name==#name")
    @GetMapping("/name")
    public String hello(String name) {
        return "hello " + name;
    }

    // filterTarget的值必须是数组、集合
    // 对传入的参数进行过滤,用户id应该为奇数
    @PreFilter(value = "filterObject.id%2!=0", filterTarget = "users")
    @PostMapping("/users")
    public void addUsers(@RequestBody List<User> users) {
        System.out.println("users = " + users);
    }

    // 用户传入的参数id=1才行
    @PostAuthorize("returnObject.id==1")
    @GetMapping("/userId")
    public User getUserById(Integer id) {
        return new User(id, "张飞");
    }

    // 对返回值进行过滤
    @PostFilter("filterObject.id%2==0")
    @GetMapping("/lists")
    public List<User> getAll() {
        List<User> users = new ArrayList<>();
        for (int i = 0;i < 10;i++) {
            users.add(new User(i, "张飞" + i));
        }

        return users;
    }


    @Secured("ROLE_ADMIN")  // 只能判断角色
    @GetMapping("/secured1")
    public User getUserByUsername1() {
        return new User(99, "secured111");
    }

    @Secured({"ROLE_ADMIN", "ROLE_USER"})  // 有其中一个即可
    @GetMapping("/secured2")
    public User getUserByUsername2() {
        return new User(99, "secured222");
    }

    @PermitAll
    @GetMapping("/permitAll")
    public String permitAll() {
        return "PermitAll";
    }

    @DenyAll
    @GetMapping("/denyAll")
    public String denyAll() {
        return "DenyAll";
    }

    @RolesAllowed({"ROLE_ADMIN", "ROLE_USER"})
    @GetMapping("/rolesAllowed")
    public String rolesAllowed() {
        return "RolesAllowed";
    }
}

三、基于数据库实战

  • 实现FilterInvocationSecurityMetadataSource
1. 数据库
/*
 Navicat Premium Data Transfer

 Source Server         : 101.43.117.227
 Source Server Type    : MySQL
 Source Server Version : 80027 (8.0.27)
 Source Host           : 101.43.117.227:3308
 Source Schema         : AuthorizeDemo1

 Target Server Type    : MySQL
 Target Server Version : 80027 (8.0.27)
 File Encoding         : 65001

 Date: 07/09/2022 20:58:29
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for menu
-- ----------------------------
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `pattern` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_as_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of menu
-- ----------------------------
INSERT INTO `menu` VALUES (1, '/admin/**');
INSERT INTO `menu` VALUES (2, '/user/**');
INSERT INTO `menu` VALUES (3, '/guest/**');

-- ----------------------------
-- Table structure for menu_role
-- ----------------------------
DROP TABLE IF EXISTS `menu_role`;
CREATE TABLE `menu_role`  (
  `id` int NOT NULL,
  `mid` int NULL DEFAULT NULL,
  `rid` int NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `menu_id`(`mid` ASC) USING BTREE,
  INDEX `role_id`(`rid` ASC) USING BTREE,
  CONSTRAINT `menu_role_ibfk_1` FOREIGN KEY (`mid`) REFERENCES `menu` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `menu_role_ibfk_2` FOREIGN KEY (`rid`) REFERENCES `role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_as_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of menu_role
-- ----------------------------
INSERT INTO `menu_role` VALUES (1, 1, 1);
INSERT INTO `menu_role` VALUES (2, 2, 2);
INSERT INTO `menu_role` VALUES (3, 3, 3);
INSERT INTO `menu_role` VALUES (4, 3, 2);

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_ci NULL DEFAULT NULL,
  `nameZh` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_as_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'ROLE_ADMIN', '系统管理员');
INSERT INTO `role` VALUES (2, 'ROLE_USER', '普通用户');
INSERT INTO `role` VALUES (3, 'ROLE_GUEST', '游客');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `usename` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_ci NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_ci NULL DEFAULT NULL,
  `enabled` tinyint(1) NULL DEFAULT NULL,
  `locked` tinyint(1) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_as_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'admin', '{noop}123', 1, 0);
INSERT INTO `user` VALUES (2, 'user', '{noop}123', 1, 0);
INSERT INTO `user` VALUES (3, 'bob', '{noop}123', 1, 0);

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role`  (
  `id` int NOT NULL,
  `uid` int NULL DEFAULT NULL,
  `rid` int NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `user_id`(`uid` ASC) USING BTREE,
  INDEX `user_role_id`(`rid` ASC) USING BTREE,
  CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`rid`) REFERENCES `role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_as_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1, 1);
INSERT INTO `user_role` VALUES (2, 1, 2);
INSERT INTO `user_role` VALUES (3, 2, 2);
INSERT INTO `user_role` VALUES (4, 3, 3);

SET FOREIGN_KEY_CHECKS = 1;

2. config层
package com.miao.config;

import com.miao.entity.Menu;
import com.miao.entity.Role;
import com.miao.service.MenuService;
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.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import java.util.Collection;
import java.util.List;

// 自定义动态权限获取
@Component
public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    MenuService menuService;

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        // 1. 当前请求对象
        String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
        // 2. 查询所有菜单结果
        List<Menu> allMenu = menuService.getAllMenu();
        for (Menu menu :  allMenu) {
            if (antPathMatcher.match(menu.getPattern(), requestURI)) {
                String[] roles = menu.getRoles().stream().map(r -> r.getName()).toArray(String[]::new);
                return SecurityConfig.createList(roles);
            }
        }

        return null;
    }

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

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}
package com.miao.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.UrlAuthorizationConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;

@Configuration
public class WebSecurityConfig {
    @Autowired
    CustomSecurityMetadataSource customSecurityMetadataSource;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        // 1. 获取工厂对象
        ApplicationContext applicationContext = httpSecurity.getSharedObject(ApplicationContext.class);
        // 2. 设置自定义过滤器(URL)权限处理
        httpSecurity.apply(new UrlAuthorizationConfigurer<>(applicationContext))
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setSecurityMetadataSource(customSecurityMetadataSource);
                        // 是否拒绝公共资源的访问,即没有被权限保护的资源
                        object.setRejectPublicInvocations(false);
                        return object;
                    }
                });

        return httpSecurity
                .formLogin()
                .and()
                .csrf().disable()
                .build();
        /*         这三个就不需要了
                .authorizeRequests().anyRequest().authenticated()
         */
    }
}
3. controller层
package com.miao.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    /*
    *  /admin/** 能被ROLE_ADMIN访问
    *  /user/** 能被ROLE_USER访问
    *  /guest/** 能被ROLE_USER ROLE_GUEST访问
    */

    /*
    *  admin ADMIN USER
    *  user USER
    *  bob GUEST
    */


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

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

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

    @RequestMapping("/hello")
    public String hello() {
        return "hello";
    }
}
4. dao层
package com.miao.dao;

import com.miao.entity.Menu;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface MenuMapper {
    List<Menu> getAllMenu();
}
package com.miao.dao;

import com.miao.entity.Role;
import com.miao.entity.User;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface UserMapper {
    // 根据用户id获取角色
    List<Role> getUserRoleById(Integer id);

    // 根据用户名获取用户
    User getUserByUsername(String username);
}
5. entity层
package com.miao.entity;

import java.util.List;

public class Menu {
    private Integer id;
    private String pattern;
    private List<Role> roles;

    public Integer getId() {
        return id;
    }

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

    public String getPattern() {
        return pattern;
    }

    public void setPattern(String pattern) {
        this.pattern = pattern;
    }

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

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

    @Override
    public String toString() {
        return "Menu{" +
                "id=" + id +
                ", pattern='" + pattern + '\'' +
                ", roles=" + roles +
                '}';
    }
}
package com.miao.entity;

public class Role {
    private Integer id;
    private String name;
    private String nameZh;

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

    public String getNameZh() {
        return nameZh;
    }

    public void setNameZh(String nameZh) {
        this.nameZh = nameZh;
    }
}
package com.miao.entity;

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

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

public class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private boolean enabled;
    private boolean locked;
    private List<Role> roles;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 将权限转换成GrantedAuthority对象
        return roles.stream().map(r -> new SimpleGrantedAuthority(r.getName())).collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }

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

    @Override
    public boolean isAccountNonLocked() {
        return !locked;
    }

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

    public Integer getId() {
        return id;
    }

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

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public boolean isLocked() {
        return locked;
    }

    public void setLocked(boolean locked) {
        this.locked = locked;
    }

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

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

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", enabled=" + enabled +
                ", locked=" + locked +
                ", roles=" + roles +
                '}';
    }
}
6. service层
package com.miao.service;

import com.miao.dao.MenuMapper;
import com.miao.entity.Menu;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MenuService {
    @Autowired
    MenuMapper menuMapper;

    public List<Menu> getAllMenu() {
        return menuMapper.getAllMenu();
    }
}
package com.miao.service;

import com.miao.dao.UserMapper;
import com.miao.entity.Role;
import com.miao.entity.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;

import java.util.List;

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 根据用户名查询用户
        User user = userMapper.getUserByUsername(username);
        // 2. 判断用户是否存在
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在!");
        }
        // 3. 根据用户id查询角色
        List<Role> roles = userMapper.getUserRoleById(user.getId());
        user.setRoles(roles);
        return user;
    }
}
7. 映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!--接口类全类名-->
<mapper namespace="com.miao.dao.MenuMapper">
    <resultMap id="MenuResultMap" type="Menu">
        <id property="id" column="id"/>
        <result property="pattern" column="pattern"/>
        <collection property="roles" ofType="Role">
            <id column="rid" property="id"/>
            <result column="rname" property="name"/>
            <result column="rnameZh" property="nameZh"/>
        </collection>
    </resultMap>

    <select id="getAllMenu" resultMap="MenuResultMap">
        select m.*, r.id as rid, r.name as rname, r.nameZh as rnameZh
        from menu m
        left join menu_role mr on m.id = mr.mid
        left join role r on r.id = mr.rid
    </select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!--接口类全类名-->
<mapper namespace="com.miao.dao.UserMapper">
    <select id="getUserRoleById" resultType="Role">
        select role.*
        from role, user_role
        where role.id = user_role.id and user_role.uid = #{uid}
    </select>

    <select id="getUserByUsername" resultType="User">
        select *
        from user
        where usename=#{username}
    </select>
</mapper>
8. 配置文件
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://101.43.117.227:3308/AuthorizeDemo1?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=123456
mybatis.mapper-locations=classpath:com/miao/mapper/*.xml
mybatis.type-aliases-package=com.miao.entity

十一、OAuth2

一、授权模式

1. 授权码模式

客户端、授权服务器、资源服务器。

2. 简化模式
3. 密码模式
4. 客户端模式

二、客户端开发(Github)

1. config层
package com.miao.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                // .formLogin() 也有效,但是丑
                .oauth2Login()  // 使用oauth2认证,在配置文件中配置认证服务
                .and()
                .build();
    }
}
2. controller层
package com.miao.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public DefaultOAuth2User hello() {
        System.out.println("hello");
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return (DefaultOAuth2User) authentication.getPrincipal();
    }
}
3. 配置文件
spring.security.oauth2.client.registration.github.client-id=3eb7e298221d49d1780c
spring.security.oauth2.client.registration.github.client-secret=50655785c0c446b78f5d901ac3cc8a535feeba5f
# 与github写的一致,且固定的,只有最后的github可以自定义(根据平台写)
spring.security.oauth2.client.registration.github.redirect-uri=http://localhost:8080/login/oauth2/code/github

十二、Spring Security OAuth2

一、客户端

二、资源服务器

三、授权服务器

十三、JWT

1. 依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

2. 起步

  1. 生成令牌
long l = System.currentTimeMillis();
Date date = new Date(l+10000);

JwtBuilder jwtBuilder = Jwts.builder()
        .setId("666")  // 设置id
        .setSubject("Test_JWT")  // 设置主题
        .setIssuedAt(new Date())  // 设置签发日期
        .setExpiration(date)  // 设置过期时间
        .claim("userid", "root")  // 自定义信息
        .signWith(SignatureAlgorithm.HS256, "miao");  // 加密和设置密钥
String jwt = jwtBuilder.compact();  // 生成令牌
System.out.println("令牌是:" + jwt);
  1. 令牌如下
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiJUZXN0X0pXVCIsImlhdCI6MTY2MTY1NzU5OX0.MK2VpLQG6j0NqikD9Q10wsdqPEYWODaMe-p6wlhvmAI
  1. 解密令牌
Claims claims = Jwts.parser().setSigningKey("miao").parseClaimsJws(jwt).getBody();
System.out.println("解密是:" + claims);

十四、拓展

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cs4m

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值