402JavaSpringSecurity5.7.5 GA

深入浅出Spring Security 王松

一、认证

  • 你是谁?
  • 用户信息由Authentication接口负责,认证工作由AuthenticationManager接口负责。
  • 用户定义由UserDetails接口负责,数据源提供由UserDetailsService接口负责。
  • 密码加密由PasswordEncoder接口负责
  • 记住我由RememberMeService接口负责

一、登录成功

  • 实际由AuthenticationSuccessHandler接口的三个实现类负责。

    • SimpleUrlAuthenticationSuccessHandler
      • SaveRequestAwareAuthenticationSuccessHandler,在SimpleUrlAuthenticationSuccessHandler基础上添加了请求缓存功能。
    • ForwardAuthenticationSuccessHandler
  • onAuthenticationSuccess方法完成。

一、defaultSuccessUrl

  • 实际由SaveRequestAwareAuthenticationSuccessHandler类实现
  • 也可由SimpleUrlAuthenticationFailureHandler类(相比较前者,少了缓存功能)实现
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index")
                .failureUrl("/login.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()  // 跟登录相关的接口不做拦截
                .and()
                .csrf().disable()
                .build();
    }
}
  1. 用户在未认证的情况下访问页面,登录成功后自动从登陆页面重定向至该页面。
  2. 用户一开始访问的就是登陆页面,登陆成功后自动从登录页面重定向至其所指定的页面。
  3. 通过自身的重载方法,设置true既可变为请求转发方式跳转页面。
  4. 通过重定向方式跳转页面。

二、successForwardUrl

  • 实际由ForwardAuthenticationSuccessHandler类实现
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/doLogin")
                .successForwardUrl("/index")
                .failureUrl("/login.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()  // 跟登录相关的接口不做拦截
                .and()
                .csrf().disable()
                .build();
    }
}
  1. 无论在什么情况下,登录成功后自动请求转发至所指定的页面。。
  2. 通过请求转发方式跳转页面。

三、successHandler

  1. 重定向
    1. 方法一:生成新的SavedRequestAwareAuthenticationSuccessHandler对象,并设置需要的值。
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/doLogin")
                // 需要在action中将doLogin改为doLogin?target=/hello
                .successHandler(savedRequestAwareAuthenticationSuccessHandler())  
                .failureUrl("/login.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()  // 跟登录相关的接口不做拦截
                .and()
                .csrf().disable()
                .build();
    }

    SavedRequestAwareAuthenticationSuccessHandler savedRequestAwareAuthenticationSuccessHandler() {
        SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();
        handler.setDefaultTargetUrl("/index");  // 设置登录成功后重定向到达的页面
        handler.setTargetUrlParameter("target");  // 设置传入参数的名称,并且登录成功后重定向至该页面

        return handler;
    }
}
  1. 请求转发

    1. 方法一:生成新的ForwardAuthenticationSuccessHandler对象,并设置需要的值。
    @Configuration
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            return http.authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginPage("/login.html")
                    .loginProcessingUrl("/doLogin")
                    .successHandler(forwardAuthenticationSuccessHandler())
                    .failureUrl("/login.html")
                    .usernameParameter("uname")
                    .passwordParameter("passwd")
                    .permitAll()  // 跟登录相关的接口不做拦截
                    .and()
                    .csrf().disable()
                    .build();
        }
    
        ForwardAuthenticationSuccessHandler forwardAuthenticationSuccessHandler() {
            ForwardAuthenticationSuccessHandler handler = new ForwardAuthenticationSuccessHandler("/hello");
            return handler;
        }
    }
    
  2. 前后端分离

  3. 方法一:创建类实现AuthenticationSuccessHandler接口,并且重写onAuthenticationSuccess方法。

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> resp = new HashMap<>();
        resp.put("status", 200);
        resp.put("msg", "登录成功!");
        ObjectMapper om = new ObjectMapper();
        String s = om.writeValueAsString(resp);
        response.getWriter().write(s);
    }
}
.successHandler(new MyAuthenticationSuccessHandler())
  1. 方法二:直接使用匿名内部类,重写onAuthenticationSuccess方法。
![AuthenticationFailureHandler](E:\Note\typora\images\AuthenticationFailureHandler.jpg).successHandler(new AuthenticationSuccessHandler() {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> resp = new HashMap<>();
        resp.put("status", 200);
        resp.put("msg", "登录成功!");
        ObjectMapper om = new ObjectMapper();
        String s = om.writeValueAsString(resp);
        response.getWriter().write(s);
    }
})

二、登录失败

  • 实际由AuthenticationFailureHandler接口的五个实现类负责。
    • SimpleUrlAuthenticationFailureHandler
      • ExceptionMappingAuthenticationFailureHandler
    • ForwardAuthenticationFailureHandler
    • AuthenticationEntryPointFailureHandler
    • DelegationAuthenticationFailureHandler
  • onAuthenticationFailure方法完成。

一、failureUrl

  • 实际由SimpleUrlAuthenticationFailureHandler类实现
@Configuration
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureUrl("/mylogin.html")  // 登录失败后仍然重定向至登录页面,只能通过url携带错误信息
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable()
                .build();
    }
}
  1. 通过重定向方式跳转页面。

二、failureForwardUrl

  • 实际由ForwardAuthenticationFailureHandler类实现
@Configuration
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureForwardUrl("/mylogin.html")  //  登录失败后请求转发至登陆页面,能够在服务器携带错误信息
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable()
                .build();
    }
}
  1. 通过请求转发方式跳转页面。

三、failureHandler

  1. 重定向
@Configuration
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureHandler(simpleUrlAuthenticationFailureHandler())
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable()
                .build();
    }

    SimpleUrlAuthenticationFailureHandler simpleUrlAuthenticationFailureHandler() {
        SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler("/mylogin.html");
        handler.setUseForward(false);  // 默认就是false。

        return handler;
    }
}
  1. 请求转发
@Configuration
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")

                .failureHandler(simpleUrlAuthenticationFailureHandler())
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable()
                .build();
    }

    SimpleUrlAuthenticationFailureHandler simpleUrlAuthenticationFailureHandler() {
        SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler("/mylogin.html");
        handler.setUseForward(true);  // 为true时,请求转发方式跳转页面。

        return handler;
    }
}
  1. 前后端分离

    1. 方法一:创建类实现AuthenticationFailureHandler接口,并且重写onAuthenticationFailure方法。
    public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            response.setContentType("application/json;charset=UTF-8");
            Map<String, Object> resp = new HashMap<>();
            resp.put("status", 500);
            resp.put("msg", "登录失败!" + exception.getMessage());
            ObjectMapper om = new ObjectMapper();
            String s = om.writeValueAsString(resp);
            response.getWriter().write(s);
        }
    }
    
    .failureHandler(new MyAuthenticationFailureHandler())
    
    1. 方法二:直接使用匿名内部类,重写onAuthenticationFailure方法。
    .failureHandler(new AuthenticationFailureHandler() {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            response.setContentType("application/json;charset=UTF-8");
            Map<String, Object> resp = new HashMap<>();
            resp.put("status", 500);
            resp.put("msg", "登录失败!" + exception.getMessage());
            ObjectMapper om = new ObjectMapper();
            String s = om.writeValueAsString(resp);
            response.getWriter().write(s);
        }
    })
    

三、注销登录

一、logoutSuccessUrl

  • 效果:只有一个注销地址和对应的结果。
@Configuration
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .mvcMatchers("/mylogin.html").permitAll()  // 放行/mylogin.html接口用于注销后跳转
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .logout()
                .logoutUrl("/logout")  // 指定注销登录的请求地址,默认GET请求且地址为/logout
                .invalidateHttpSession(true)  // 是否使session失效,默认为true
                .clearAuthentication(true)  // 是否清楚认证信息,默认为true
                .logoutSuccessUrl("/mylogin.html")  // 注销后跳转的地址
                .and()
                .csrf().disable().build();
    }
}

二、logoutRequestMatcher

  • 效果:设置多个注销地址及对应的请求方法。

三、logoutSuccessHandler

  1. 前后端分离

    • 效果:不同注销地址对应同一结果
    @Configuration
    public class SecurityConfig {
        @Bean
        SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            return http.authorizeRequests()
                    // .mvcMatchers("/mylogin.html").permitAll()  // 放行/mylogin.html接口用于注销后跳转
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .and()
                    .logout()
                    .logoutRequestMatcher(new OrRequestMatcher(
                            new AntPathRequestMatcher("/logout1", "GET"), // 可以使用GET请求注销登录
                            new AntPathRequestMatcher("/logout2", "POST")  // 可以使用POST请求注销登录
                    ))
                    .invalidateHttpSession(true)  // 是否使session失效,默认为true
                    .clearAuthentication(true)  // 是否清楚认证信息,默认为true
                    .logoutSuccessHandler(new LogoutSuccessHandler() {
                        @Override
                        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                            response.setContentType("application/json;charset=UTF-8");
                            Map<String, Object> resp = new HashMap<>();
                            resp.put("status", 200);
                            resp.put("msg", "注销成功!");
                            ObjectMapper om = new ObjectMapper();
                            String s = om.writeValueAsString(resp);
                            response.getWriter().write(s);
                        }
                    })
                    .and()
                    .csrf().disable().build();
        }
    }
    

四、defaultLogoutSuccessHandlerFor

  1. 前后端分离

    • 效果:不同注销地址对应不同的结果。
    @Configuration
    public class SecurityConfig {
        @Bean
        SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            return http.authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .and()
                    .logout()
                    .logoutRequestMatcher(new OrRequestMatcher(
                            new AntPathRequestMatcher("/logout1", "GET"), // 可以使用GET请求注销登录
                            new AntPathRequestMatcher("/logout2", "POST")  // 可以使用POST请求注销登录
                    ))
                    .invalidateHttpSession(true)  // 是否使session失效,默认为true
                    .clearAuthentication(true)  // 是否清楚认证信息,默认为true
                    .defaultLogoutSuccessHandlerFor(new LogoutSuccessHandler() {
                        @Override
                        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                            response.setContentType("application/json;charset=UTF-8");
                            Map<String, Object> resp = new HashMap<>();
                            resp.put("status", 200);
                            resp.put("msg", "注销成功!111111");
                            ObjectMapper om = new ObjectMapper();
                            String s = om.writeValueAsString(resp);
                            response.getWriter().write(s);
                        }
                    }, new AntPathRequestMatcher("/logout1", "GET"))
                    .defaultLogoutSuccessHandlerFor(new LogoutSuccessHandler() {
                        @Override
                        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                            response.setContentType("application/json;charset=UTF-8");
                            Map<String, Object> resp = new HashMap<>();
                            resp.put("status", 200);
                            resp.put("msg", "注销成功!2222222");
                            ObjectMapper om = new ObjectMapper();
                            String s = om.writeValueAsString(resp);
                            response.getWriter().write(s);
                        }
                    }, new AntPathRequestMatcher("/logout2", "POST"))
                    .and()
                    .csrf().disable().build();
        }
    }
    

四、获取用户数据

一、SecurityContextHolder

一、单线程
    @GetMapping("/user")
    public String userInfo() {
        Authentication authentication  = SecurityContextHolder.getContext().getAuthentication();
        return authentication.toString();
    }
二、多线程

二、当前请求对象

    @GetMapping("/authentication")
    public String authentication(Authentication authentication) {
        return authentication.toString();
    }

    @GetMapping("/principal")
    public String principal(Principal principal) {
        return principal.toString();
    }

五、用户定义及数据源

一、InMemoryUserDetailsManager

  • 自定义数据源
  1. 方法一:

    • @Autowired
      public void userDetailsService(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
          InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
          UserDetails user = User.withUsername("root").roles("admin").password("{noop}12345").build();
          manager.createUser(user);
          authenticationManagerBuilder.userDetailsService(manager);
      }
      
  2. 方法二:

    • @Bean
      public UserDetailsService userDetailsService() {
          // InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
          UserDetails user1 = User.withUsername("root").roles("root").password("{noop}123").build();
          UserDetails user2 = User.withUsername("admin").roles("admin").password("{noop}12345").build();
          return new InMemoryUserDetailsManager(user1, user2);
          // manager.createUser(user1);
          // manager.createUser(user2);
          // return manager;
      }
      

二、JdbcUserDetailsManager

  • 自定义数据源
  1. 方法一:

    • @Autowired
      public void Jdbc(AuthenticationManagerBuilder auth, DataSource dataSource) throws Exception {
          JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
          if (!manager.userExists("root")) {
              manager.createUser(User.withUsername("root").roles("root").password("{noop}12345").build());
          }
          if (!manager.userExists("admin")) {
              manager.createUser(User.withUsername("admin").roles("admin").password("{noop}123456").build());
          }
          auth.userDetailsService(manager);
      }
      
  2. 方法二:

    • @Bean
      public UserDetailsService Jdbc(DataSource dataSource) {
          JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
          if (!manager.userExists("root")) {
              manager.createUser(User.withUsername("root").roles("root").password("{noop}12345").build());
          }
          if (!manager.userExists("admin")) {
              manager.createUser(User.withUsername("admin").roles("admin").password("{noop}123456").build());
          }
          return manager;
      }
      

三、MyBatis

  • 自定义用户
  • 自定义数据源
CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL,
  `nameZh` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `role` WRITE;
/*!40000 ALTER TABLE `role` DISABLE KEYS */;

INSERT INTO `role` (`id`, `name`, `nameZh`)
VALUES
	(1,'ROLE_dba','数据库管理员'),
	(2,'ROLE_admin','系统管理员'),
	(3,'ROLE_user','用户');

/*!40000 ALTER TABLE `role` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table user
# ------------------------------------------------------------

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `enabled` tinyint(1) DEFAULT NULL,
  `accountNonExpired` tinyint(1) DEFAULT NULL,
  `accountNonLocked` tinyint(1) DEFAULT NULL,
  `credentialsNonExpired` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `user` WRITE;
/*!40000 ALTER TABLE `user` DISABLE KEYS */;

INSERT INTO `user` (`id`, `username`, `password`, `enabled`, `accountNonExpired`, `accountNonLocked`, `credentialsNonExpired`)
VALUES
	(1,'root','{noop}123',1,1,1,1),
	(2,'admin','{noop}123',1,1,1,1),
	(3,'sang','{noop}123',1,1,1,1);

/*!40000 ALTER TABLE `user` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table user_role
# ------------------------------------------------------------

CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `uid` (`uid`),
  KEY `rid` (`rid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `user_role` WRITE;
/*!40000 ALTER TABLE `user_role` DISABLE KEYS */;

INSERT INTO `user_role` (`id`, `uid`, `rid`)
VALUES
	(1,1,1),
	(2,1,2),
	(3,2,2),
	(4,3,3);
@Configuration
public class SecurityConfig {
    @Autowired
    MyUserDetailsService myUserDetailsService;
    
    // 以下代码可不用写,因为当自定义用户放在IOC容器中,默认会使用它
    @Autowired
    public void MyBatis(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService);
    }
}
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.ArrayList;
import java.util.Collection;
import java.util.List;

public class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean accountNonExpired;
    private Boolean accountNonLocked;
    private Boolean credentialsNonExpired;
    private List<Role> roles = new ArrayList<>();

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

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

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

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

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

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

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

    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 Boolean getEnabled() {
        return enabled;
    }

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

    public Boolean getAccountNonExpired() {
        return accountNonExpired;
    }

    public void setAccountNonExpired(Boolean accountNonExpired) {
        this.accountNonExpired = accountNonExpired;
    }

    public Boolean getAccountNonLocked() {
        return accountNonLocked;
    }

    public void setAccountNonLocked(Boolean accountNonLocked) {
        this.accountNonLocked = accountNonLocked;
    }

    public Boolean getCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
        this.credentialsNonExpired = credentialsNonExpired;
    }

    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 +
                ", accountNonExpired=" + accountNonExpired +
                ", accountNonLocked=" + accountNonLocked +
                ", credentialsNonExpired=" + credentialsNonExpired +
                ", 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;
    }

    @Override
    public String toString() {
        return "Role{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", nameZh='" + nameZh + '\'' +
                '}';
    }
}
package com.miao.entity;

import com.miao.mapper.UserMapper;
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.Objects;

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

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);
        if (Objects.isNull(user)) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
        user.setRoles(userMapper.getRolesByUid(user.getId()));
        return user;
    }
}
@Mapper
public interface UserMapper {
    List<Role> getRolesByUid(Integer id);
    User loadUserByUsername(String username);
}
<?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.mapper.UserMapper">
    <select id="loadUserByUsername" resultType="User">
        SELECT * FROM user WHERE username=#{username}
    </select>
    <select id="getRolesByUid" resultType="Role">
        SELECT r.* FROM role r, user_role ur WHERE r.id = ur.rid
    </select>
</mapper>
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://101.43.117.227:3308/Security2?userUnicode=UTF-8
spring.datasource.username=root
spring.datasource.password=123456

mybatis.type-aliases-package=com.miao.entity
mybatis.mapper-locations=classpath:**/*.xml
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

四、Spring Data JPA

六、密码加密

  • PasswordEncoder接口

    • BCryptPasswordEncoder实现类
    • ArgonnePasswordEncoder实现类
    • Pbkdf2PassEncoder实现类
    • SCryptPassEncoder实现类
    • DelegatingPasswordEncoder工具类(由PasswordEncoderFactories类创建,且默认加密方式为BCryptPasswordEncoder)
  • 全局的和局部的AuthenticationManager都是使用同一个DelegatingPasswordEncoder工具类或者BCryptPasswordEncoder实现类

一、加密

  • 方案一:使用DelegatingPasswordEncode(默认使用BCryptPasswordEncoder),DelegatingPasswordEncode#encode,DelegatingPasswordEncode#matches+xxxEncoder#matches(默认是BCryptPasswordEncoder#matches)
@Configuration
public class SecurityConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("{bcrypt}$2a$10$5PxLt7t8yieNMGFG4dTxiOXjoWWed2MxdOevyW3bLXVoQh7.lUGNO").build());
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable().build();
    }
}
  • 方案二:使用BCryptPasswordEncoder,BCryptPasswordEncoder#encode,BCryptPasswordEncoder#matches
@Configuration
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("$2a$10$5PxLt7t8yieNMGFG4dTxiOXjoWWed2MxdOevyW3bLXVoQh7.lUGNO").build());
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable().build();
    }
}

二、加密方案自动升级

  • PasswordEncoder#upgradeEncoding、DaoAuthenticationProviderDaoAuthenticationProvider#createSuccessAuthentication
SELECT * FROM `user`CREATE DATABASE IF NOT EXISTS Security5 COLLATE utf8mb4_unicode_ci;

USE Security5;

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT INTO `user` (`id`, `username`, `password`)
VALUES (1,'javaboy','{noop}123');

public class User implements UserDetails {
    private Long id;
    private String username;
    private String password;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

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

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

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

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

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

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

    public Long getId() {
        return id;
    }

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

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

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

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}
@Mapper
public interface UserMapper {
    User loadUserByUsername(String username);
    Integer updatePassword(@Param("username") String username, @Param("newPassword") String newPassword);
}
<?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.mapper.UserMapper">
    <select id="loadUserByUsername" resultType="User">
        SELECT * FROM user WHERE username = #{username}
    </select>
    <update id="updatePassword">
        UPDATE user SET password = #{newPassword} WHERE username = #{username}
    </update>
</mapper>
@Service
public class UserService implements UserDetailsService, UserDetailsPasswordService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userMapper.loadUserByUsername(username);
    }

    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
       Integer result = userMapper.updatePassword(user.getUsername(), newPassword);
       if (result == 1) {
           ((User) user).setPassword(newPassword);  // 将最新的密码替换掉旧密码,用于后续认证
       }
       return user;
    }
}
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable().build();
    }
}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://101.43.117.227:3308/Security5?userUnicode=UTF-8
spring.datasource.username=root
spring.datasource.password=123456

mybatis.type-aliases-package=com.miao.entity
mybatis.mapper-locations=classpath:**/*.xml
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

七、RememberMe

  • key若不手动设置,则会生成UUID,每次都会更新,导致之前的remember-me失效。

一、RememberMe

  • 重启服务器后不能自动登录
@Configuration
public class SecurityConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("{noop}123").build());
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .rememberMe()
                .key("mykey")  // 选择性使用
                .and()
                .csrf().disable().build();
    }
}

二、持久化令牌

  • 重启服务器后能自动登录
@Configuration
public class SecurityConfig {
    @Autowired
    private DataSource dataSource;

    public JdbcTokenRepositoryImpl jdbcTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);  // 该方法需要jdbc依赖
        return jdbcTokenRepository;
    }

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .rememberMe()
                .key("mykey")  // 必须使用,不然重启服务器后之前的remember-me用不了
                .tokenRepository(jdbcTokenRepository())  // 指定JdbcTokenRepositoryImpl实例
                .and()
                .csrf().disable().build();
    }
}

三、二次检验

  • 重启服务器后能自动登录
@Configuration
public class SecurityConfig {
    @Autowired
    private DataSource dataSource;

    public JdbcTokenRepositoryImpl jdbcTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .antMatchers("/admin").fullyAuthenticated()
                .antMatchers("/rememberme").rememberMe()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .rememberMe()
                .key("mykey")  // 必须使用,不然重启服务器后之前的remember-me用不了
                .tokenRepository(jdbcTokenRepository())
                .and()
                .csrf().disable().build();
    }
}
@RestController
public class MyController {
    @GetMapping("/hello")  // 认证后可以访问,无论何种认证方式
    public String hello() {
        return "hello";
    }

    @GetMapping("/admin") // 认证后可以访问,必须是用户名/密码方式
    public String admin() {
        return "admin";
    }

    @GetMapping("/rememberme")  // 必须先rememberme,然后重启浏览器才能访问
    public String rememberme() {
        return "rememberme";
    }
}

八、会话管理

一 、会话并发

  1. 重定向
@Configuration
public class SecurityConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("{noop}123").build());
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .antMatchers("/hi").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable()
                .sessionManagement()  // 开启会话并发
                .maximumSessions(1)  // 最大并发数
                .expiredUrl("/hi")  // 并挤下线后重定向的地址
                .and()
                .and().build();
    }

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();  // 用于维护当前HttpSession记录
    }
}
  1. 禁止后来者登录
@Configuration
public class SecurityConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("{noop}123").build());
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .antMatchers("/hi").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable()
                .sessionManagement()  // 开启会话并发
                .maximumSessions(1)  // 最大并发数
                .maxSessionsPreventsLogin(true)  // 禁止后来者登录
                .and()
                .and().build();
    }

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();  // 用于维护当前HttpSession记录
    }
}
  1. 前后端分离
@Configuration
public class SecurityConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("{noop}123").build());
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .antMatchers("/hi").permitAll()
                .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();
                        response.setContentType("application/json;charset=UTF-8");
                        Map<String, Object> resp = new HashMap<>();
                        resp.put("status", 500);
                        resp.put("msg", "当前会话已经失效,请重新登录");
                        String s = new ObjectMapper().writeValueAsString(resp);
                        response.getWriter().write(s);
                    }
                })
                .and()
                .and().build();
    }

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();  // 用于维护当前HttpSession记录
    }
}

二、会话固定攻击与防御

三、Session共享

  • 集群环境下使用

九、HttpFirewall

一、严格模式

二、普通模式

十、漏洞保护

一、CSRF攻击与防御

二、HTTP响应头处理

一、缓存控制
二、X-Content-Type-Options
三、Strict-Transport-Security
四、X-Frame-Options
五、X-XSS-Protection
六、Content-Security-Policy
七、Referrer-Policy
八、Feature-policy
九、Clear-Site-Data

三、HTTP通信安全

一、HTTPS
1. 非正常使用
  • 生成证书
keytool -genkey -alias tomcathttps -keyalg RSA -keysize 2048 -keystore myhttps.p12 -validity 365
口令:111111
是否正确:y
  • 配置证书
server.ssl.key-store=classpath:myhttps.p12
server.ssl.key-alias=tomcathttps
server.ssl.key-store-password=111111
  • 访问
https://localhost:8080/login
输入:thisisunsafe
2. 正常使用
  • http默认端口是8080,https默认端口为8443。(即访问http://localhost:8080/https会转发到https://localhost:8443/https)
  1. 项目端口与https默认端口一致(8443)(即访问http://localhost:8080/https会转发到https://localhost:8443/https)
server.ssl.key-store=classpath:myhttps.p12
server.ssl.key-alias=tomcathttps
server.ssl.key-store-password=111111
server.port=8443
@Configuration
public class TomcatConfig {
    @Bean
    TomcatServletWebServerFactory tomcatServletWebServerFactory() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.addAdditionalTomcatConnectors(createTomcatConnector());

        return factory;
    }

    private Connector createTomcatConnector() {
        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        connector.setPort(8080);  // 监听8080端口
        return connector;
    }
}
@Configuration
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .requiresChannel()  // 开启配置
                .antMatchers("/https").requiresSecure()  // 该接口的请求是https协议,若不是则使用https协议重定向https://localhost:8443/https
                .antMatchers("/http").requiresInsecure()  // 该接口的请求是http协议,若不是则使用http协议重定向http://localhost:8080/http
                .and()
                .csrf().disable().build();
    }
}
  1. 项目端口与https默认端口不一致(8443)(即访问http://localhost:8080/https会转发到https://localhost:8443/https,而不是https://localhost:8444/https)
server.ssl.key-store=classpath:myhttps.p12
server.ssl.key-alias=tomcathttps
server.ssl.key-store-password=111111
server.port=8444
@Configuration
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .portMapper()  // 开启配置
                .http(8080).mapsTo(8444) // 设置8080端口转发至8444端口
                .and()
                .requiresChannel()
                .antMatchers("/https").requiresSecure()
                .antMatchers("/http").requiresInsecure()
                .and()
                .csrf().disable().build();
    }
}
二、代理服务器配置

十一、HTTP认证

一、HTTP Basic authentication

  • HTTP基本认证,不安全
@Configuration
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .httpBasic()  // 不再formLogin()
                .and()
                .csrf().disable().build();
    }
}

二、HTTP Digest authentication

  • HTTP摘要认证,安全,但不支持BCrypt、PBKDF2、SCrypt加密方式

十二、跨域问题

一、Spring处理方案

一、@CrossOrigin
  • DisptacherServlet中触发,可与addCorsMappings合并使用
@RestController
public class MyController {
    /*
    * allowCredentials:浏览器是否应当发送凭证信息,如Cookie。
    * allowedHeaders:请求被允许的请求头字段,*表示所有字段。
    * expossedHeaders:哪些响应头可以作为响应的一部分暴露出来。注意,这里可以一一列举,通配符*无效。
    * maxAge:预检请求的有效期,有效期内不必再次发生预检请求,默认是1800秒。
    * methods:允许的请求方法,*表示允许所有方法。
    * origins:允许的域,*表示允许所有的域。
    * */
    @CrossOrigin(origins = "http://localhost:8081")
    @PostMapping("/post")
    public String post() {
        return "hello post";
    }
}
二、addCorsMappings
  • DisptacherServlet中触发,可与@CrossOrigin合并使用

  • 重写WebMvcConfigurer#addCorsMappings

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedMethods("*")
                .allowedOrigins("*")
                .allowedHeaders("*")
                .allowCredentials(false)
                .exposedHeaders("")
                .maxAge(3600);
    }
}
三、CorsFilter
  • 过滤器中触发,早于前两种方式触发,与前两者一起用会降低性能
@Configuration
public class WebMvcConfigFilter {
    @Bean
    FilterRegistrationBean<CorsFilter> corsFilterFilterRegistrationBean() {
        FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        corsConfiguration.setAllowedMethods(Arrays.asList("*"));
        corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:8081"));
        corsConfiguration.setMaxAge(3600L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        registrationBean.setFilter(new CorsFilter(source));
        registrationBean.setOrder(-1);
        
        return registrationBean;
    }
}

二、Spring Security处理方案

一、特殊处理OPTIONS请求
  • 确保在Spring Security中能正常使用Spring中前两种方法

  • 不安全且不优雅,了解即可

@Configuration
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS).permitAll()  // 放行OPTIONS请求,使@CorsOrigin和重写addCorsMappings方法有效
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable().build();
    }
}
二、CorsFilter
  • 只需要将该过滤器优先级高于Spring Security过滤器优先级即可
@Configuration
public class WebMvcConfigFilter {
    @Bean
    FilterRegistrationBean<CorsFilter> corsFilterFilterRegistrationBean() {
        FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        corsConfiguration.setAllowedMethods(Arrays.asList("*"));
        corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:8081"));
        corsConfiguration.setMaxAge(3600L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        registrationBean.setFilter(new CorsFilter(source));
        registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);  // 设置为最高优先级

        return registrationBean;
    }
}
三、*专业处理方案
@Configuration
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .cors()
                .configurationSource(configurationSource())
                .and()
                .csrf().disable().build();
    }
    CorsConfigurationSource configurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        corsConfiguration.setAllowedMethods(Arrays.asList("*"));
        corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:8081"));
        corsConfiguration.setMaxAge(3600L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        
        return source;
    }
}

十三、异常处理

  • 主要由过滤器ExceptionTranslationFilter处理

一、认证异常

  • AuthenticationException

二、权限异常

  • AccessDeniedException

三、自定义异常

一、不使用默认处理器
  • 所有接口共用同一个处理方案
@Configuration
public class SeccurityConfig {
    @Bean
    UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(User.withUsername("root").roles("user").password("{noop}123").build());
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .antMatchers("/admin").hasRole("admin")  // 需要有admin角色才能访问admin接口
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .defaultSuccessUrl("/admin")
                .permitAll()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(new AuthenticationEntryPoint() {
                    @Override
                    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                        response.setStatus(HttpStatus.UNAUTHORIZED.value());
                        response.getWriter().write("please login");
                    }
                })  // 认证异常
                .accessDeniedHandler(new AccessDeniedHandler() {
                    @Override
                    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                        response.setStatus(HttpStatus.FORBIDDEN.value());
                        response.getWriter().write("forbidden");
                    }
                })  // 权限异常
                .and()
                .csrf().disable().build();
    }
}
二、使用默认处理器
  • 不同接口可对应不同处理方案
@Configuration
public class SeccurityConfig {
    @Bean
    UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(User.withUsername("wx").roles("wxadmin").password("{noop}123").build(),
                User.withUsername("qq").roles("user").password("{noop}123").build()
                );
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        AntPathRequestMatcher matcher1 = new AntPathRequestMatcher("/wx/admin");
        AntPathRequestMatcher matcher2 = new AntPathRequestMatcher("/qq/admin");
        return http.authorizeRequests()
                .antMatchers("/wx/admin").hasRole("wxadmin")
                .antMatchers("/qq/admin").hasRole("qqadmin")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .defaultSuccessUrl("/index")
                .permitAll()
                .and()
                .exceptionHandling()
                .defaultAuthenticationEntryPointFor(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("请登录,微信用户");
                    }
                }, matcher1)
                .defaultAuthenticationEntryPointFor(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("请登录,QQ用户");
                    }
                }, matcher2)
                .defaultAccessDeniedHandlerFor(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("权限不足,微信用户");
                    }
                }, matcher1)
                .defaultAccessDeniedHandlerFor(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("权限不足,QQ用户");
                    }
                }, matcher2)
                .and()
                .csrf().disable().build();
    }
}

二、授权

  • 你可以做什么?

一、权限管理

一、核心
一、前置处理器

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

二、后置处理器
三、权限元数据
四、权限表达式
二 、基于URL地址(过滤器)的权限管理
一、基本使用
@RestController
public class MyController {
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }

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

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

    @GetMapping("/getinfo")
    public String getInfo() {
        return "getinfo";
    }
}
@Configuration
public class SecurityConfig {
    @Bean
    UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(User.withUsername("admin").roles("ADMIN").password("{noop}123").build(),
                User.withUsername("root").roles("USER").password("{noop}123").build(),
                User.withUsername("reader").authorities("READ_INFO").password("{noop}123").build());
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")  // 必须具有ADMIN角色才可以访问,会自动加上ROLE_前缀
                .antMatchers("/user/**").access("hasAnyRole('USER', 'ADMIN')")  // 使用access来使用权限表达式,拥有USER、ADMIN任意角色都可以访问,会自动加上ROLE_前缀
                .antMatchers("/getinfo").hasAuthority("READ_INFO")  // 必须具有READ_INFO权限才可以访问,不会加上ROLE_前缀
                .anyRequest().access("isAuthenticated()")  // 是要是认证过的用户都可以访问
                .and()
                .formLogin()
                .and()
                .csrf().disable().build();
    }
}
二、角色继承
@Configuration
public class SecurityConfig {
    @Bean
    RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
        hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");  // 角色ADMIN继承了角色USER
        return hierarchy;
    }

    @Bean
    UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(User.withUsername("admin").roles("ADMIN").password("{noop}123").build(),
                User.withUsername("root").roles("USER").password("{noop}123").build(),
                User.withUsername("reader").authorities("READ_INFO").password("{noop}123").build());
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")  // 必须具有ADMIN角色才可以访问,会自动加上ROLE_前缀
                .antMatchers("/user/**").access("hasAnyRole('USER')")  // 使用access来使用权限表达式,拥有USER角色可以访问,ADMIN也可以访问,因为继承了USER,会自动加上ROLE_前缀
                .antMatchers("/getinfo").hasAuthority("READ_INFO")  // 必须具有READ_INFO权限才可以访问,不会加上ROLE_前缀
                .anyRequest().access("isAuthenticated()")  // 是要是认证过的用户都可以访问
                .and()
                .formLogin()
                .and()
                .csrf().disable().build();
    }
}
三、自定义表达式
  • 注意第一个字母要变成小写的!!!如MyExpression类:@myExpression
@RestController
public class MyController {
    @GetMapping("/hello/{userId}")
    public String hello(@PathVariable Integer userId) {
        return "hello" + userId;
    }

    @GetMapping("/hi")
    public String hello2User(String username) {
        return "hello" + username;
    }
}
@Component
public class MyExpression {
    public boolean checkId(Authentication authentication, Integer userId) {
        // 如果通过认证,才进行授权鉴定
        if (authentication.isAuthenticated()) {
            // 要求id必须为偶数
            return userId % 2 == 0;
        } else {
            return false;
        }
    }

    public boolean check(HttpServletRequest request) {
        // 要求请求参数username必须为root
        // localhost:8080/hi?username=root
        return "root".equals(request.getParameter("username"));
    }
}
@Configuration
public class SecurityConfig {
    @Bean
    UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(User.withUsername("root").roles("USER").password("{noop}123").build());
    }
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .antMatchers("/hello/{userId}")
                .access("@myExpression.checkId(authentication, #userId)")
                .antMatchers("/hi")
                .access("isAuthenticated() and @myExpression.check(request)")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable().build();
    }
}
四、动态权限管理
# ************************************************************
# Sequel Pro SQL dump
# Version 4541
#
# http://www.sequelpro.com/
# https://github.com/sequelpro/sequelpro
#
# Host: 127.0.0.1 (MySQL 5.7.26)
# Database: security13
# Generation Time: 2020-09-22 08:10:28 +0000
# ************************************************************


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;


# Dump of table menu
# ------------------------------------------------------------

DROP TABLE IF EXISTS `menu`;

CREATE TABLE `menu` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `pattern` varchar(128) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `menu` WRITE;
/*!40000 ALTER TABLE `menu` DISABLE KEYS */;

INSERT INTO `menu` (`id`, `pattern`)
VALUES
	(1,'/admin/**'),
	(2,'/user/**'),
	(3,'/guest/**');

/*!40000 ALTER TABLE `menu` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table menu_role
# ------------------------------------------------------------

DROP TABLE IF EXISTS `menu_role`;

CREATE TABLE `menu_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `mid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `mid` (`mid`),
  KEY `rid` (`rid`),
  CONSTRAINT `menu_role_ibfk_1` FOREIGN KEY (`mid`) REFERENCES `menu` (`id`),
  CONSTRAINT `menu_role_ibfk_2` FOREIGN KEY (`rid`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `menu_role` WRITE;
/*!40000 ALTER TABLE `menu_role` DISABLE KEYS */;

INSERT INTO `menu_role` (`id`, `mid`, `rid`)
VALUES
	(1,1,1),
	(2,2,2),
	(3,3,3),
	(4,3,2);

/*!40000 ALTER TABLE `menu_role` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table role
# ------------------------------------------------------------

DROP TABLE IF EXISTS `role`;

CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL,
  `nameZh` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `role` WRITE;
/*!40000 ALTER TABLE `role` DISABLE KEYS */;

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

/*!40000 ALTER TABLE `role` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table user
# ------------------------------------------------------------

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `enabled` tinyint(1) DEFAULT NULL,
  `locked` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `user` WRITE;
/*!40000 ALTER TABLE `user` DISABLE KEYS */;

INSERT INTO `user` (`id`, `username`, `password`, `enabled`, `locked`)
VALUES
	(1,'admin','{noop}123',1,0),
	(2,'user','{noop}123',1,0),
	(3,'javaboy','{noop}123',1,0);

/*!40000 ALTER TABLE `user` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table user_role
# ------------------------------------------------------------

DROP TABLE IF EXISTS `user_role`;

CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `uid` (`uid`),
  KEY `rid` (`rid`),
  CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `user` (`id`),
  CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`rid`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `user_role` WRITE;
/*!40000 ALTER TABLE `user_role` DISABLE KEYS */;

INSERT INTO `user_role` (`id`, `uid`, `rid`)
VALUES
	(1,1,1),
	(2,1,2),
	(3,2,2),
	(4,3,3);

/*!40000 ALTER TABLE `user_role` ENABLE KEYS */;
UNLOCK TABLES;



/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
1.非所有URL必须配置数据库
2.所有URL必须配置数据库
三、基于方法(AOP)的权限管理

二、权限模型

一、ACL

二、RBAC


三、OAuth2

  • 第三方登录

一、授权

一、四种授权模式

二、第三方登录

一、github
@Configuration
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .oauth2Login()  // 开启OAuth2登录
                .and()
                .csrf().disable().build();
    }
}
@RestController
public class MyController {
    @GetMapping("/hello")
    public DefaultOAuth2User github() {
        return ((DefaultOAuth2User) SecurityContextHolder.getContext().getAuthentication().getPrincipal());
    }

    @GetMapping("/demo1")
    public String demo1() {
        return "demo1";
    }
}
spring.security.oauth2.client.registration.github.client-id=3eb7e298221d49d1780c
spring.security.oauth2.client.registration.github.client-secret=356e413e7b28bd62bc64327c0ce79bb11504711e
二、gitee

三、授权服务器与资源服务器

二、Redis

三、JWT

四、认证流程分析

一、认证五类(自下而上)

一、AuthenticationManager接口类

  • ProviderManager实现类(默认)。
  • 自定义实现类。

二、ProviderManager实现类

  • 可以管理多个AuthenticationProvider接口类(列表)

    • private List<AuthenticationProvider> providers
      
  • 优先使用本类中的AuthenticationProvider提供的认证方法,都认证失败,则调用父类的认证方法(AuthenticationManager或其子类(ProviderManager实现类))

三、*AuthenticationProvider接口类

  • 核心认证类,提供认证方法,可以放入ProviderManager中管理。

  • 用户名/密码认证:DaoAuthenticationProvider实现类(继承AbstractUserDetailsAuthenticationProvider抽象类,并且具体认证逻辑也在这个抽象类中)。

    • 核心认证方法是authenticate,在其中调用其他方法的顺序:preAuthenticationChecks---->additionalAuthenticationChecks—>postAuthenticationChecks—>createSuccessAuthentication(创建UsernamePasswordAuthenticationToken对象)
  • 记住我认证:RememberMeAuthenticationProvider实现类。

四、Authentication接口类

  • 用户名/密码认证:UsernamePasswordAuthenticationToken实现类

五、AbstractAuthenticationProcessingFilter抽象类

abstractauthenticationprocessingfilter

  • 用户名/密码认证UsernamePasswordAuthenticationFilter实现类
  • 由于Authentication对应的实例是UsernamePasswordAuthenticationtoken类型,AuthenticationManager对应的实例是ProviderManager类型,故流程图:UsernamePasswordAuthenticationFilter(登录参数在此提取)—>UsernamePasswordAuthenticationtoken—>ProviderManager

二、配置多个数据源

  • 每个AuthenticationProvider中都有一个UserDetailsService。
  1. 在AuthenticationProvider(DaoAuthenticationProvider实现类)中设置两个数据源,再放入ProviderManager的集合中。
@Configuration
public class SecurityConfig {
    @Bean
    public AuthenticationManager authenticationManager() {
        UserDetails user1 = User.withUsername("root").roles("root").password("{noop}1234").build();
        UserDetails user2 = User.withUsername("admin").roles("admin").password("{noop}12345").build();
        InMemoryUserDetailsManager u1 = new InMemoryUserDetailsManager(user1);
        InMemoryUserDetailsManager u2 = new InMemoryUserDetailsManager(user2);
        DaoAuthenticationProvider dao1 = new DaoAuthenticationProvider();
        DaoAuthenticationProvider dao2 = new DaoAuthenticationProvider();
        dao1.setUserDetailsService(u1);
        dao2.setUserDetailsService(u2);

        return new ProviderManager(dao1, dao2);
    }
}

三、添加验证码

public class KaptchaAuthenticationProvider extends DaoAuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        HttpServletRequest req = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();  // 获取当前请求对象
        String kaptcha = req.getParameter("kaptcha");  // 获取页面提交的验证码文本
        String sessionKaptcha = (String) req.getSession().getAttribute("kaptcha");  // 获取存储在session中的验证码文本(在验证码图像生成前就放入了session)
        if (Objects.nonNull(kaptcha) && Objects.nonNull(sessionKaptcha) && kaptcha.equalsIgnoreCase(sessionKaptcha)) {
            return super.authenticate(authentication);
        }
        throw new AuthenticationServiceException("验证码输入错误");
    }
}
@Configuration
public class KaptchaConfig {
    @Bean
    public Producer kaptcha() {
        Properties properties = new Properties();
        properties.setProperty("kaptcha.image.width", "150");
        properties.setProperty("kaptcha.image.height", "50");
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);

        return defaultKaptcha;
    }
}
@Configuration
public class SecurityConfig {
    @Bean
    public AuthenticationManager authenticationManager() {
        KaptchaAuthenticationProvider provider = new KaptchaAuthenticationProvider();
        provider.setUserDetailsService(new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("{noop}12345").build()));
        return new ProviderManager(provider);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .antMatchers("/vc.jpg").permitAll()  // 对验证码图片接口放行
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index")
                .failureForwardUrl("/mylogin.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable().build();
    }
}
@Controller
public class MyController {
    @Autowired
    private Producer producer;

    @GetMapping("/vc.jpg")
    public void getVerifyCode(HttpServletResponse response, HttpSession session) {
        response.setContentType("image/jpeg");
        String text = producer.createText();  // 生成验证码文本,并存入session中用来后面的验证
        session.setAttribute("kaptcha", text);
        BufferedImage image = producer.createImage(text); // 根据验证码文本生成验证码图片
        try {
            ServletOutputStream out = response.getOutputStream();
            ImageIO.write(image, "jpg", out);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

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

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

四、注意点

  • 默认有局部的AuthenticaionManager(有AnonymousProvider)
  • 默认有全局的AuthenticationManager(有DaoAuthenticationProvider,即生成初始user及其密码)
一、全局设置AuthenticationMananger、AuthenticationProvider、UserDetailsService
  1. @Bean返回AuthenticationManager,则是GlobalAuthenticationManager
  • 即没有user,GlobalAuthenticationManager被代替了
@Bean
ProviderManager provider() {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(new InMemoryUserDetailsManager(User.withUsername("root").roles(("root")).password("{noop}123").build()));
    ProviderManager manager = new ProviderManager(provider);
    return manager;
}
  1. @Bean返回AuthenticationProvider,则是为GlobalAuthenticationManager配备AuthenticationProvider。
  • 即没有user,userDetailsService被自定义的代替了
@Bean
AuthenticationProvider provider() {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(new InMemoryUserDetailsManager(User.withUsername("root").roles(("root")).password("{noop}123").build()));
    return provider;
}
  1. @Bean返回userDetailsService,改变了GlobalAuthenticationManager中的userDetailsService
  • 即没有user,userDetailsService被自定义的代替了
@Bean
UserDetailsService userDetailsService() {
    return new InMemoryUserDetailsManager(User.withUsername("root").roles(("root")).password("{noop}123").build());
}
二、局部设置AuthenticationMananger、AuthenticationProvider、UserDetailsService
  1. 局部设置AuthenticationMananger优先级高于@Bean,GlobalAuthenticationManager仍然存在但是不再生效
  • 即仍然有user,但是无法进行使用
.authenticationManager(manager());
  1. 局部设置AuthenticationProvider配备给局部LocalAuthenticationProvider(则此时不仅有AnonymousProvider,还有DaoAuthenticationProvider)
  • 即仍然有user,且能够正常使用
.authenticationProvider(provider())
  1. 局部设置userDetailsService,会自动生成一个AuthenticationProvider配备给LocalAuthenticationManager(则此时不仅有AnonymousProvider,还有DaoAuthenticationProvider)
  • 即仍然有user,且能够正常使用
.userDetailsService(userDetailsService())

五、过滤器链流程分析

优先级排序:

  • ForceEagerSessionCreationFilter
  • ChannelProcessingFilter
  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter
  • CorsFilter
  • CsrfFilter
  • LogoutFilter
  • OAuth2AuthorizationRequestRedirectFilter
  • Saml2WebSsoAuthenticationRequestFilter
  • X509AuthenticationFilter
  • AbstractPreAuthenticatedProcessingFilter
  • CasAuthenticationFilter
  • OAuth2LoginAuthenticationFilter
  • Saml2WebSsoAuthenticationFilter
  • UsernamePasswordAuthenticationFilter
  • DefaultLoginPageGeneratingFilter
  • DefaultLogoutPageGeneratingFilter
  • ConcurrentSessionFilter
  • DigestAuthenticationFilter
  • BearerTokenAuthenticationFilter
  • BasicAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • JaasApiIntegrationFilter
  • RememberMeAuthenticationFilter
  • AnonymousAuthenticationFilter
  • OAuth2AuthorizationCodeGrantFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor
  • SwitchUserFilter

一、JSON登录

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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class LoginFilter 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());
        }
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equalsIgnoreCase("application/json;charset=UTF-8")) {

            Map<String, String> userInfo = new HashMap<>();
            try {
                userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String username = userInfo.get(getUsernameParameter());
                String password = userInfo.get(getPasswordParameter());
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                        username, password);
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return super.attemptAuthentication(request, response);
    }
}
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.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.parameters.P;
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.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;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class SecurityConfig {

    // 全局AuthenticationManager
    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;
    @Bean
    public UserDetailsService GlobaluserDetailsService() {
        return new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("{noop}12345").build());
    }


    // 局部AuthenticationManager
    public AuthenticationManager LocalauthenticationManager() {
        DaoAuthenticationProvider dao = new DaoAuthenticationProvider();
        dao.setUserDetailsService(LoacluserDetailsService());
        ProviderManager manager = new ProviderManager(dao);
        return manager;
    }
    public UserDetailsService LoacluserDetailsService() {
        return new InMemoryUserDetailsManager(User.withUsername("admin").roles("admin").password("{noop}12345").build());
    }

    public LoginFilter loginFilter() throws Exception {
        LoginFilter filter = new LoginFilter();
        // 全局AuthenticationManager
        // filter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
        // 局部AuthenticationManager
        AuthenticationManager authenticationManager = LocalauthenticationManager();
        filter.setAuthenticationManager(authenticationManager);
        filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                response.setContentType("application/json;charset=UTF-8");
                Map<String, Object> resp = new HashMap<>();
                resp.put("status", 200);
                resp.put("msg", "登录成功" + authentication);
                ObjectMapper om = new ObjectMapper();
                String s = om.writeValueAsString(resp);
                response.getWriter().write(s);
            }
        });

        return filter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class)
                .csrf().disable().build();
    }
}

二、添加验证码

@Configuration
public class SecurityConfig {
    @Autowired
    public AuthenticationConfiguration authenticationConfiguration;

    public LoginFilter loginFilter() throws Exception {
        LoginFilter filter = new LoginFilter();
        filter.setFilterProcessesUrl("/doLogin");  // 设置该过滤器生效地址,默认是/login
        filter.setUsernameParameter("uname");  // 必须在此处设置,在http中设置不会生效
        filter.setPasswordParameter("passwd");  // 必须在次数设置,在http中设置不会生效
        filter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
        filter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("/index"));
        filter.setAuthenticationFailureHandler(new ForwardAuthenticationFailureHandler("/mylogin.html"));  // 将会显示错误信息

        return filter;
    }

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeRequests()
                .antMatchers("/vc.jpg").permitAll()  // 对验证码图片接口放行
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
//                .usernameParameter("uname") // 有自定义过滤器的情况下不生效
//                .passwordParameter("passwd")  // 有自定义过滤器的情况下不生效
                .permitAll()
                .and()
                .addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class)
                .csrf().disable().build();
    }
}
public class LoginFilter 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());
        }
        String kaptcha = request.getParameter("kaptcha");
        String sessionKaptcha = (String) request.getSession().getAttribute("kaptcha");
        if (Objects.nonNull(kaptcha) && Objects.nonNull(sessionKaptcha) && kaptcha.equalsIgnoreCase(sessionKaptcha)) {
            return super.attemptAuthentication(request, response);  // 仍然使用表单登录方式认证
        }
        throw new AuthenticationServiceException("验证码输入错误");
    }
}
  • 23
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

cs4m

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

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

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

打赏作者

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

抵扣说明:

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

余额充值