编辑历史:

  2021/01/06 15:13 :第三点“单体springboot项目怎么进行多认证方式”,增加一张原理说明图。

 

正文:

    一个正常的系统应用,都会要求安全性,不能让人随便乱搞。我们在开发web应用时,怎么保护我们的资源,这是十分重要的。

  在以前jsp / servlet 时代,我们可能会直接在每个servlet上都加上用户身份验证,也会有系统是通过Filter来验证用户身份。

  现在web应用中,主要有两套安全框架:shiro 和 spring security。

  功能上两者都差不多,shiro有的功能spring security都有,而且spring security也还有一些额外的功能,例如对Oahtu的支持

spring security 安全框架

 

上图是网上的一些对比,如果是做单体项目,shiro足以,如果是分布式项目,推荐spring security 和 oauth2.0

 

下面我将会从四个方面讲解spring security的使用:

1:单体springboot项目怎么用?

2:spring security 的认证过程!

3:单体springboot项目怎么进行多认证方式?

4:分布式项目怎么用spring security?

 

 

1:单体springboot项目怎么用?

1、引依赖(红色是重点


<dependencies>
<!--security的starter-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!--security 的测试包-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

<!--spring mvc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

<!--lombok小辣椒-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!--测试包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

</dependencies>
 

 

 

其实我们只要一引入spring security的依赖,整个项目的web请求都会被spring security拦截。

我们写个启动类,再写个web接口,启动项目

@SpringBootApplication
@RestController
public class SpringSecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityApplication.class, args);
    }
    
    @RequestMapping("/say/{name}")
    public String echo(@PathVariable String name){
        return "这里是控制器:" + name;
    }

}

 

访问 http://localhost:9999/say/123 时就会被重定向到一个登陆页面

spring security 安全框架

 

 

 这个时候你看一下控制台,就会发现多了一串这玩意

spring security 安全框架

 

 

 这是spring security 自动帮你创建的一个随机密码,用户名是user,登陆进去后就能正常访问我们的接口了。

spring security不止提供了一个默认的登陆页面,也提供了一个登出接口:http://localhost:9999/logout

有些版本会直接登出,有些新版本会有下面这个页面。

spring security 安全框架

 

 

 

当然我们的正常项目肯定是有自己的用户模块,不可能用它自带的东西。下面将讲怎样接入我们自己的用户模块。

 

首先假设我们有一个用户实体类

@Getter
@Setter
@AllArgsConstructor
@ToString
public class UserDO{
    private Integer id;
    private String userName;
    private String password;
    private String realName;
    private List<String> roles;
    private List<String> permissions;
}

 

然后呢我们有一个用户业务类,专门去数据库查询用户的。这里我们不连数据库,我懒,不想弄,直接用一个Map充当数据库

@Service
public class UserServiceImpl {

  // 正经的业务代码
    public UserDO getUserByUserName(String userName){
        if(userList == null){
            return null;
        }
        UserDO userDO = userList.get(userName);
        return userDO;
    }

  // 用来模拟数据库的一个Map
    private static Map<String,UserDO> userList;


    public UserServiceImpl(){
        initUserList();
    }


    /**
     * 模拟数据库用户
     * */
    private void initUserList(){
        if (userList == null){
            userList = new HashMap<>(3);
            userList.put("zhangsan",new UserDO(1,"zhangsan",password_123,"张三"
                    , Arrays.asList("admin","role1"),Arrays.asList("p1","p2","p3")));
            userList.put("lisi",new UserDO(2,"lisi",password_123,"李四"
                    , Arrays.asList("role1"),Arrays.asList("p1","p2")));
            userList.put("wangwu",new UserDO(2,"wangwu",password_123,"王五"
                    , Arrays.asList("role2"),Arrays.asList("p3","p2")));
        }
    }


    private static final String password_123 = "$2a$10$a0iYBZkmfqJnhd0g5ck9L.kfcf9RpdHFJ.mt5wf2sN2qzA6y9k/BC";

}

 

 

接下来就是重点了。

建一个类继承 WebSecurityConfigurerAdapter  ,进行spring security的配置

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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.annotation.Resource;

/**
 * 这个是spring security 的web安全配置类,这个类必须有
 * @author hongcheng
 */
@Configuration
@EnableWebSecurity      // 启动web的安全控制,这个注解好象不用也行
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsService userDetailsService;


    /**
     * 配置认证管理器
     * */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
     // 配置 userDetailsService ,这是用来查询用户信息的 auth.userDetailsService(userDetailsService);
// 设置擦除密码,如果设置成false,那么在spring security中流转的用户认证信息都会带有密码,一般我们都会擦除 auth.eraseCredentials(
true); super.configure(auth); } /** * 配置url安全认证拦截的 * */ @Override protected void configure(HttpSecurity http) throws Exception { /* * csrf().disable() 关闭跨域请求限制,如果开启,可能会出现很多非get请求出现405 * .authorizeRequests() 启动请求认证 * .antMatchers("/**").authenticated() 匹配指定的url地址进行认证判断 * .anyRequest().permitAll() 对其他地址进行放行 * formLogin() 启动表单登录 * .loginPage("/login.html") 可以自定义登录页面 * .failureUrl("/login_fail.html") 表单登录失败的跳转地址 * .defaultSuccessUrl("/login_s.html",true) 表单登录成功的跳转地址, * 参数2如果为false,登录成功时会跳转到拦截前的页面,true时登录成功固定跳转给定的页面 * .logout() 启动用户退出,security提供了默认退出地址:/logout * .logoutSuccessUrl("/logout.html") 成功推出后的跳转地址 * */ http.csrf().disable() .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .failureUrl("/login_fail.html") .defaultSuccessUrl("/login_s.html",true) .permitAll() // permitAll表示登录相关的请求都放开,一定要加,不然你连登录页面都看不到 .and() .logout() .logoutSuccessUrl("/logout.html")
permitAll(); // 这里也要加,不然你退出后就看不到退出页面了 }
/** * 这个是配置密码编码器的<br/> * NoOpPasswordEncoder表示不进行密码编码 * */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 这个是我自己写来加密密码的 * */ public static void main(String[] args) { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); System.err.println(passwordEncoder.encode("123")); } }

 

 

这是spring security的配置文件,大部分你们都能看懂,我说说这个url拦截配置

这两个是一起用的,表示对哪些具体的url进行拦截,也可以模糊匹配
.authorizeRequests()
.anyRequest().authenticated()

这个and用于分割不同的拦截策略,例如上面的配置就用了两个and方法,因为他有三组拦截策略,一组是针对所有url的,一组是针对登录表单的,一组是针对登出的 .and()

这个是登录的,spring security默认提供了一个登录接口 http://localhost:9999/login 你也可以自己看登录页面的源码,至于这个接口的实现在哪里,后面会详细讲,这里我们只需要直到默认有这个接口就行 .formLogin() .failureUrl("/login_fail.html") .defaultSuccessUrl("/login_s.html",true) .permitAll()
spring security 安全框架spring security 安全框架

 

     这是登出的,也有提供默认接口 http://localhost:9999/logout

 .logout()
 .logoutSuccessUrl("/logout.html")
.permitAll();

 

配置完了后还有一步很关键,就是要告诉spring security去哪里获取你的用户信息

我们自己建一个类,继承 UserDetailsService

import com.hongcheng.springsecurity.entity.UserDO;
import com.hongcheng.springsecurity.service.user.UserServiceImpl;
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;

import javax.annotation.Resource;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 这个是配置用户详情查询的,用于查询用户,给security用的
 * @author hongcheng
 */
@Component
public class MyUserDetailsServiceImpl implements UserDetailsService {

// 这个是我们自己的业务类,用来查数据库 @Resource
private UserServiceImpl userService; /** * 这个方法是一定要重写的,spring security会根据页面传回来的用户名去查这个用户的权限和密码 * */ @Override public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { UserDO userDO = userService.getUserByUserName(userName); if(userDO == null){ throw new UsernameNotFoundException("账号不存在"); } Set<String> authritiesSet = new HashSet<>(userDO.getPermissions().size() + userDO.getRoles().size()); authritiesSet.addAll(userDO.getPermissions()); // 这里需要注意,security里面角色和权限都是存在一个字段里面的,但是其角色会自动加上ROLE_前缀来将角色和权限进行区分, // 以便在进行判断是否有某角色时可以进行判断,但是我们一般使用是可以不用加前缀的,如@PreAuthorize("hasRole('role2')"),但是你加也没问题 authritiesSet.addAll(userDO.getRoles().stream().map(role -> "ROLE_" + role).collect(Collectors.toList())); UserDetails userDetails = User.withUsername(userDO.getUserName()) .password(userDO.getPassword()) .authorities(authritiesSet.toArray(new String[authritiesSet.size()])) .build(); return userDetails; } }

 

UserDetailsService 是spring security提供的一个自定义查询用户的接口,和shiro的 Realm 是相同作用的 

另外spring security封装了一些认证异常,并不是说你想抛啥就抛啥,当然,你随便抛也行,程序直接异常而已。

用spring security提供的异常,spring security就会帮你捕获,并提示页面。都是 AuthenticationException 的子类

spring security 安全框架

 

 

完了之后你就可以启动项目了,然后自己去测试。

这时你启动项目,你会发现没有随机密码了。用自己设置的账号和密码登录,测试下不同情况吧。

 

 spring security不仅可以将针对web请求进行拦截,还可以对具体方法进行拦截

  web拦截:

  web url的拦截是要在security的配置类中写的,而且一定要注意,拦截规则是从上往下的,如果前面的拦截规则包含的url返回比下面的还要大,例如:

.antMatchers("/r/**").hasAuthority("p1")  
.antMatchers("/r/r1").hasAuthority("p2")  
这种情况下,/r/r1 拦截规则是必须有p1权限
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()                                                            // 开启请求认证
            .antMatchers("/r/r1").hasAuthority("p1")                                        // /r/r1 地址的请求需要有p1权限
            .antMatchers("/r/r2").hasAnyRole("p2")                                          // /r/r2 地址的请求需要有p2角色
            .antMatchers("/r/r3").access("hasAuthority('p1') and hasAuthority('p2')")       // /r/r3 地址的请求需要同时有p1和p2这两个权限
            .antMatchers("/r/**").authenticated()                                           // /r/** /r开头的其他请求都需要认证            .anyRequest().permitAll()                                                       // 剩余没有说明的地址全部开放,不用认证        .and()                    .formLogin()         // ...       }

 

保护URL常用的方法有:
      authenticated() 保护URL,需要用户登录
      permitAll() 指定URL无需保护,一般应用与静态资源文件
      hasRole(String role) 限制单个角色访问,角色将被增加 “ROLE_” .所以”ADMIN” 将和 “ROLE_ADMIN”进行比较.
      hasAuthority(String authority) 限制单个权限访问
      hasAnyRole(String… roles)允许多个角色访问.
      hasAnyAuthority(String… authorities) 允许多个权限访问.
      access(String attribute) 该方法使用 SpEL表达式, 所以可以创建复杂的限制.
      hasIpAddress(String ipaddressExpression) 限制IP地址或子网

 

 方法拦截:

 这种方式主要是往类或者方法上面加注解来限制对该方法的访问

 Spring Security在方法的控制权限上支持三种类型的注解,JSR-250注解、@Secured注解和支持表达式的注解

 

JSR-250注解包括:@RolesAllowed、@PermitAll、@DenyAll

  @RolesAllowed:表示访问对应方法时所应具有的角色 ,例如:@RolesAllowed({“User”,“ADMIN”}) 该方法只要具有"User","Admin"任意一种权限就可以访问。这里可以省略前缀ROLE_,实际的权限可能是ROLE_ADMIN,也可能是ADMIN

  @PermitAll表示允许所有的角色进行访问,也就是说不进行权限的控制

  @DenyAll和@PermitAll相反的,表示无论什么角色都不能访问

 

@Secured注解:这个注解和@RolesAllowed的作用是一样的,不过@Secured不能省略前缀ROLE_,必须@Secured({“ROLE_User”,“ROLE_ADMIN”}) 

 

支持表达式的注解:@PreAuthorize、@PostAuthorize

@PreAuthorize:这是进入方法前进行权限判断,常用

@PostAuthorize:这是方法执行后进行权限判断,不常用

值可以是spring el表达式,一般我们都是用org.springframework.security.access.expression.SecurityExpressionRoot这个类里面的方法进行权限判断,例如:

    @RequestMapping("/say/{name}")
    @PreAuthorize("hasAnyAuthority('p1')")
    public String echo(@PathVariable String name){
        return "这里是控制器:" + name;
    }

 

 

需要注意的还有一点:spring security默认不启用方法注解权限判断,我们必须手动加上。找个加了 @Configuration 注解的类,给他加上下面这个注解,你要用什么注解,你就设置他为true就行

@EnableGlobalMethodSecurity(prePostEnabled = true,jsr250Enabled = true,securedEnabled = true)     // 启动基于方法注解的控制

 

 

到目前为止,一个单体springboot项目使用spring security就算是完了。

 

 

接下来我们总计一下spring security的单体项目使用流程:

  1.   引入依赖 spring-boot-starter-security 
  2.        编写一个类继承 WebSecurityConfigurerAdapter
  3.        配置spring security,包括认证配置器、url拦截配置、密码匹配器
  4.        编写一个类继承 UserDetailsService ,以实现用户信息的查找
  5.        给响应的方法加权限注解(可选)

 

 

自己看着写个小例子玩玩,后面我们会讲一下spring security的整体执行流程。

 

 

 

 2:spring security 的认证过程!

 

源码原理:

  要了解他的执行流程,我们就需要调试代码。首先我们知道UserDetailsService 这个的子类,就是我们自己实现的那个类,是用来获取我们自己的用户信息的,那么登录时肯定会执行到这里,所以我们打个断点,启动项目,登录。

spring security 安全框架

 

 

 

进入断点后我们一路看调用栈,因为我们的目标是看spring security的调用,所以我们一路往下找,找到最早被调用的security相关的地方

spring security 安全框架

 

 

 这里我们发现 org.springframework.security.web.FilterChainProxy#doFilter 这里是最早调用的security的方法。我们看下代码

spring security 安全框架

 

 

 是不是和我们以前学的servlet时的Filter的写法是一样的。那我们看下这个类的情况

spring security 安全框架

spring security 安全框架

 这里是不是就可以看出了,spring security 的 FilterChainProxy 这个类,实际上就是Filter的实现类,所以你是不是明白了,spring security的实现其实就是基于Filter来实现的。

 

好了,我们继续往下追踪,进入 doFilterInternal 这个方法看下

spring security 安全框架

 

 

 spring security 安全框架

 

 

 在 doFilterInternal  我们看到有意思的地方有三处。我们一一分析。

spring security 安全框架

 

 

 我们发现第一次有意思的地方只是在验证请求的url是否正常,所以我们不管他先。看第二个地方

spring security 安全框架spring security 安全框架

 这里主要是遍历这个过滤器链,判断他和request是否匹配,究竟是匹配什么呢?我们进入match方法里面看看

 spring security 安全框架

 

 继续往下看,我们会发现有很多实现类,和security相关的也比较多,那我们就先不管他,继续看第三个地方。

我们先看这一句: FilterChainProxy.VirtualFilterChain vfc = new FilterChainProxy.VirtualFilterChain(fwRequest, chain, filters);

发现VirtualFilterChain 是一个内部类,我们看下这个内部类,看他的属性

originalChain :看参数名和实参,不难发现这就是实际的Filter过滤器链
additionalFilters : 这是一个额外的过滤器链,我们找下传入的实参是什么,发现这就是我们第二步match匹配返回的一个过滤器链 List<Filter> filters = this.getFilters((HttpServletRequest)fwRequest);

spring security 安全框架

 

 现在回想一下,是不是大概明白了点:一个spring security写的 servlet.Filter 的过滤器,里面有个 SecurityFilterChain 类型的集合,在执行 doFilter 方法时悄悄 把List<SecurityFilterChain > 和 request 进行某种匹配

然后自己拿真实的 servlet.Filter 的过滤器链,和 security自己的SecurityFilterChain 链组合,形成一个新的虚拟过滤器链 VirtualFilterChain。

 

好,现在我们继续回来,接下来就不是执行servlet.Filter 的过滤器链了,而是刚创建的虚拟过滤器链 VirtualFilterChain

spring security 安全框架

我们进去这个方法里面,仔细读下代码,你会发现它分成两部分,上面部分执行servlet.Filter原始过滤器链的,下面执行security的额外过滤器链的。

当然我们通过他打印的日志也能发现,这里是先执行额外链,后执行原始链的。

reached end of additional filter chain; proceeding with original chain

spring security 安全框架

至于为什么是先执行额外链 ,后执行原始链呢?相比研究过 servlet 的 的Filter的人都知道,在执行Filter链时,每个Filter都是请求进来通过一次,返回响应通过一次,如果请求进来的时候你不处理这个额外链,以后就没机会了。但是如果你先执行额外链,

 由于 nextFilter.doFilter(request, response, this); 所以接下来执行的都是  VirtualFilterChain 里面的额外链,执行完了后还能继续重新回到原始的 servlet.Filter 链

 

我们看下 VirtualFilterChain 里面都有啥

spring security 安全框架

 

 

 spring security 安全框架

 

 

 

 

我们接着根据线程调用栈往下找security相关的,发现他在一个个执行 additionalFilters 中的Filter 。之前不是说过security 提供了一个登录接口 /login 和一个登出接口 /logout 么,这里看 LogoutFilter 觉得是不是巧合,进去里面看一下代码。

spring security 安全框架

 

 

看下红框标出的地方,熟不熟悉 

spring security 安全框架

 

 

 联想一下前面提到的  RequestMatcher 和 需要根据servlet 和 Filter进行某系匹配,匹配某个东西。现在是不是就明白了,那同样的道理,能不能找到 /login 在哪里。

 

继续往下看,找到 UsernamePasswordAuthenticationFilter,你就找到了 /login 了,接下来我们将会重点分析这里

spring security 安全框架

 

 

 仔细找你会发现 UsernamePasswordAuthenticationFilter 并没有 doFilter 方法,我们去他父类 AbstractAuthenticationProcessingFilter 上找

spring security 安全框架

 

 

 spring security 安全框架

 

 

 这个方法里会去捕获一些认证异常,如果我们胡乱抛出,就不能被是为认证失败了,所以我们要遵守规则 

 好了,我们看一下关键语句:

authResult = attemptAuthentication(request, response);

 

我们点击去看看具体实现,发现他又跳到了 UsernamePasswordAuthenticationFilter 这里来

spring security 安全框架

 

 关键的地方就这两个点,1是创建了个Token,2是调用 AuthenticationManager 的 authenticate方法 ,去对传入的账号吗和密码进行认证

 

我们先看下 AuthenticationManager 是个啥玩意

spring security 安全框架

 

 看看他有啥具体实现类先:

spring security 安全框架

 

 行吧,没啥好玩的,那 authenticate方法我们也不知道看哪个实现类,跳过,继续看调用栈。

 

继续往下看调用栈,就发现他走的是 org.springframework.security.authentication.ProviderManager#authenticate 这个方法,那我们进去看看代码

代码太长了,我就截关键点

spring security 安全框架

 

这里我们可以看到 他在遍历,遍历啥呢? AuthenticationProvider 这是啥,不知道,但是看英文意思是认证提供者,先猜测会不会是用来认证的。

然后看下面那行, provider.supports(toTest) 在判断是否支持,支持啥,我们前面看到的  UsernamePasswordAuthenticationToken  ,如果支持就执行  result = provider.authenticate(authentication);  

那综合来讲,这段代码的意思是不是说“遍历所有 AuthenticationProvider 认证提供者,判断其是否支持 UsernamePasswordAuthenticationToken ,如果支持就执行  AuthenticationProvider 认证提供者的 authenticate 认证方法”。

 

我们继续看看 ProviderManager 里面有多少个提供者。

spring security 安全框架

 

 

好,继续走线程调用栈

spring security 安全框架

 

 我们发现此时执行的是 DaoAuthenticationProvider 的父类的 authenticate 方法,我们看下这个方法具体代码

spring security 安全框架

 

 spring security 安全框架

 

 spring security 安全框架

 

 这个方法里面有四个地方需要重点关注:

user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
postAuthenticationChecks.check(user);

 

我们先看第一个,发现其在父类中是个抽象方法,具体实现在子类

spring security 安全框架

 

 你找下他返回的 UserDetailService 就会发现这其实就是我们自己自定义的UserDetailService

spring security 安全框架

 

 结合前面的,其实就是先根据Token去缓存中找一下,如果找不到,就通过我们自己的定义的 UserDetailService 实现类,去数据库里面查,并组装返回一个UserDetail对象。

 

那接下来我们看下

preAuthenticationChecks.check(user);

 

spring security 安全框架

 

 spring security 安全框架

 

 spring security 安全框架

 

 发现这两个先后检查都是内部类,主要检查用户是否被锁定,是否过期超时,是否被禁用,密码凭证是否过期超时。

 

最后看下 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);

 

 spring security 安全框架

 

 现在整个方法的内容基本都看完了,整理下 org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate 做的事

判断传入的认证对象是不是UsernamePasswordAuthenticationToken

尝试去缓存中获取UserDetail

缓存中获取不到,就通过自定义UserDetailService去查数据库

前认证检查,检查用户是否被锁,是否过期超时,是否被禁用

额外认证检查,检查数据库密码和输入密码是否匹配

后认证检查,检查密码凭证是否过期超时

重新生成一个认证成功对象

 

 

 

spring security的整个认证流程算是完了,我们来总结一下

spring security的认证流程

1:基于servlet.filter的过滤器
    org.springframework.security.web.FilterChainProxy#doFilter

2:内部自己的过滤器链,会根据当前请求的url,将匹配的认证过滤器筛选保留
    org.springframework.security.web.FilterChainProxy.VirtualFilterChain#doFilter

3:spring security自己的抽象过滤器
    org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter

4:spring security默认的一个账号密码认证过滤器,每个认证过滤器都会绑定一个登录处理url和提交方式(post)
    org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication

5:由第4步组装一个token对象,传入第5步。ProviderManager会根据token的Class类型来判断使用哪个Provider
    org.springframework.security.authentication.ProviderManager#authenticate

6:由具体的AuthenticationProvider来负责认证用户信息
    org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate

7:UserDetailsService在AuthenticationProvider中获取用户信息
    org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername

spring security 安全框架

 

 (这里我偷黑马一张图)

 

总结下出现过的重要的类和接口

1:FilterChainProxy
    基于servlet.filter的过滤器,用于生成spring security自己内部的过滤器链

2:FilterChainProxy.VirtualFilterChain
    spring security的虚拟过滤器链,用于进行额外的过滤器链处理

3:AbstractAuthenticationProcessingFilter
    spring security额外过滤器链的父类,可以指定该过滤器处理哪些请求(url和请求method),同时在这里根据请求参数组装一个AbstractAuthenticationToken的实现类,并传给后续操作

4:UsernamePasswordAuthenticationFilter
    spring security提供的默认用户名密码过滤器链,处理POST方式的/login请求,构建UsernamePasswordAuthenticationToken类

5:ProviderManager
    用于管理AuthenticationProvider列表,根据传入的AbstractAuthenticationToken的实现类的不同,遍历AuthenticationProvider列表选择合适的AuthenticationProvider进行认证

6:AbstractUserDetailsAuthenticationProvider
    AuthenticationProvider的父类,提供了基本的认证过程代码

7:DaoAuthenticationProvider
    spring security提供的默认用户名密码认证器,处理UsernamePasswordAuthenticationToken类型的认证

8:UserDetailsService
    spring security提供的用于自定义用户查询方式的接口

9:AbstractAuthenticationToken
    spring security的认证Token的基类,所有Token都必须继承这个基类

10:UsernamePasswordAuthenticationToken
    spring security提供的默认用户名密码认证Token

 

 

3:单体springboot项目怎么进行多认证方式?

  根据前面的说明,我们可以知道spring security提供了一个默认的用户名密码认证方式,这个用户名密码认证方式是怎么工作的呢?

1:UsernamePasswordAuthenticationToken   extends AbstractAuthenticationToken
    spring security提供的默认用户名密码认证Token

2:UsernamePasswordAuthenticationFilter   extends AbstractAuthenticationProcessingFilter
    spring security提供的默认用户名密码过滤器链,处理POST方式的/login请求,构建UsernamePasswordAuthenticationToken类

3:DaoAuthenticationProvider   extends AbstractUserDetailsAuthenticationProvider
    spring security提供的默认用户名密码认证器,处理UsernamePasswordAuthenticationToken类型的认证

4:MyUserDetailsService   extends UserDetailsService
    自定义的用户查询类

 spring security 安全框架

 

 

据我们所了解的 ,AbstractAuthenticationProcessingFilter 和 AbstractUserDetailsAuthenticationProvider的实现类有多个,那我们能不能也自己实现一个,然后放进去呢?

 

模拟手机短信验证码登录,我们先实现 AbstractAuthenticationProcessingFilter ,一些具体的代码我们先抄 UsernamePasswordAuthenticationFilter 的先,后期跟进需要进行修改

/**
 * @author hongcheng
 */
public class SmsAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {

    public static final String SPRING_SECURITY_FORM_PHONE_KEY = "phone";
    public static final String SPRING_SECURITY_FORM_SMSCODE_KEY = "smsCode";

    private String phoneParameter = SPRING_SECURITY_FORM_PHONE_KEY;
    private String smsCodeParameter = SPRING_SECURITY_FORM_SMSCODE_KEY;
    private boolean postOnly = true;


    public SmsAuthenticationProcessingFilter() {
        super(new AntPathRequestMatcher("/sms/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {
        if (postOnly && !"POST".equals(request.getMethod())) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String phone = request.getParameter(this.phoneParameter);
        String smsCode = request.getParameter(this.smsCodeParameter);

        if (phone == null) {
            phone = "";
        }

        if (smsCode == null) {
            smsCode = "";
        }

        phone = phone.trim();

        SmsAuthenticationToken authRequest = new SmsAuthenticationToken(phone, smsCode);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

    protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }
}

 

期间发现需要用到Token的实现类,我们在实现一个 AbstractAuthenticationToken,基本的代码也是抄 UsernamePasswordAuthenticationToken 的进行修改

/**
 * @author hongcheng
 */
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = -6437322217156360297L;

    private final Object phone;
    private Object smsCode;


    public SmsAuthenticationToken(Object phone, Object smsCode) {
        super(null);
        this.phone = phone;
        this.smsCode = smsCode;
        setAuthenticated(false);
    }


    public SmsAuthenticationToken(Object phone, Object smsCode,
                                     Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.phone = phone;
        this.smsCode = smsCode;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return this.smsCode;
    }

    @Override
    public Object getPrincipal() {
        return this.phone;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }else {
            super.setAuthenticated(false);
        }
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        smsCode = null;
    }

}

 

 

接下来我们实现 AbstractUserDetailsAuthenticationProvider,也是抄 DaoAuthenticationProvider

/**
 * @author hongcheng
 */
public class SmsAuthenticationProvider implements AuthenticationProvider {

    private PasswordEncoder passwordEncoder;
    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        try{
            String phone = (String) authentication.getPrincipal();
            String smsCode = (String)authentication.getCredentials();
            WebAuthenticationDetails details = (WebAuthenticationDetails)authentication.getDetails();

            // 判断手机号是否存在
            UserDetails userDetails = userDetailsService.loadUserByUsername(phone);

            // 判断验证码是否一致
            HttpSession httpSession = ServletUtil.getHttpSession();
            // 已经提前将手机验证码存在了session中,key为手机号
            Object smsCodeObj = httpSession.getAttribute(phone);
            if(smsCodeObj == null){
                throw new BadCredentialsException("手机验证码错误");
            }
            String smsCodeInSession = (String)smsCodeObj.toString();
            if(!StringUtils.hasText(smsCodeInSession) || !smsCodeInSession.equalsIgnoreCase(smsCode)){
                throw new BadCredentialsException("手机验证码错误");
            }
            httpSession.removeAttribute(phone);
            // 构建返回的用户登录成功的token
            return new SmsAuthenticationToken(userDetails.getUsername(), smsCode, userDetails.getAuthorities());
        }catch (Exception e){
            if(e instanceof AuthenticationException){
                throw e;
            }else{
                throw new AuthenticationServiceException("认证服务异常");
            }
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        /**
         * providerManager会遍历所有
         * security config中注册的provider集合
         * 根据此方法返回true或false来决定由哪个provider
         * 去校验请求过来的authentication
         */
        return (SmsAuthenticationToken.class
                .isAssignableFrom(authentication));
    }

    public PasswordEncoder getPasswordEncoder() {
        return passwordEncoder;
    }

    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

 

最后我们再额外实现一个 UserDetailsService,根据手机号进行查询用户信息

/**
 * @author hongcheng
 */
@Service("SmsUserDetailsService")
public class SmsUserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private UserServiceImpl userService;


    @Override
    public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
        UserDO userDO = userService.getUserByPhone(phone);
        if(userDO == null){
            throw new UsernameNotFoundException("手机号不存在");
        }
        Set<String> authritiesSet = new HashSet<>(userDO.getPermissions().size() + userDO.getRoles().size());

        authritiesSet.addAll(userDO.getPermissions());
        // 这里需要注意,security里面角色和权限都是存在一个字段里面的,但是其角色会自动加上ROLE_前缀来将角色和权限进行区分,
        // 以便在进行判断是否有某角色时可以进行判断,但是我们一般使用是可以不用加前缀的,如@PreAuthorize("hasRole('role2')"),但是你加也没问题
        authritiesSet.addAll(userDO.getRoles().stream().map(role -> "ROLE_" + role).collect(Collectors.toList()));

        UserDetails userDetails = User.withUsername(userDO.getPhone())
                .password(userDO.getPhone())
                .authorities(authritiesSet.toArray(new String[authritiesSet.size()]))
                .build();
        return userDetails;
    }
}

 


/**
* @author hongcheng */ @Service public class UserServiceImpl { public UserDO getUserByUserName(String userName){ if(userNameList == null){ return null; } UserDO userDO = userNameList.get(userName); return userDO; } public UserDO getUserByPhone(String phone){ if(userPhoneList == null){ return null; } UserDO userDO = userPhoneList.get(phone); return userDO; } private static Map<String,UserDO> userNameList; private static Map<String,UserDO> userPhoneList; public UserServiceImpl(){ initUserList(); } /** * 模拟数据库用户 * */ private void initUserList(){ if (userNameList == null){ userNameList = new HashMap<>(3); userNameList.put("zhangsan",new UserDO(1,"zhangsan",password_123,"张三" ,"10086", Arrays.asList("admin","role1"),Arrays.asList("p1","p2","p3"))); userNameList.put("lisi",new UserDO(2,"lisi",password_123,"李四" ,"10010", Arrays.asList("role1"),Arrays.asList("p1","p2"))); userNameList.put("wangwu",new UserDO(2,"wangwu",password_123,"王五" ,"10000", Arrays.asList("role2"),Arrays.asList("p3","p2"))); userPhoneList = new HashMap<>(3); userPhoneList.put("10086",new UserDO(1,"zhangsan",password_123,"张三" ,"10086", Arrays.asList("admin","role1"),Arrays.asList("p1","p2","p3"))); userPhoneList.put("10010",new UserDO(2,"lisi",password_123,"李四" ,"10010", Arrays.asList("role1"),Arrays.asList("p1","p2"))); userPhoneList.put("10000",new UserDO(2,"wangwu",password_123,"王五" ,"10000", Arrays.asList("role2"),Arrays.asList("p3","p2"))); } } private static final String password_123 = "$2a$10$a0iYBZkmfqJnhd0g5ck9L.kfcf9RpdHFJ.mt5wf2sN2qzA6y9k/BC"; }

 

/**
 * 模拟资源
 * @author hongcheng
 */
@RestController
@RequestMapping("/test")
public class TestController {

    /**
     * @PreAuthorize("hasAnyAuthority('p1')")
     * 具体可以使用哪些方法,自己看  MethodSecurityExpressionRoot  这个类
     * */
    @PreAuthorize("hasAnyAuthority('p1')")
    @RequestMapping("/r1")
    public String test1(){
        return "这里是资源1,只能p1权限访问";
    }

    @PreAuthorize("hasAnyAuthority('p2')")
    @RequestMapping("/r2")
    public String test2(){
        return "这里是资源2,只能p2权限访问";
    }

    @PreAuthorize("hasAnyAuthority('p3')")
    @RequestMapping("/r3")
    public String test3(){
        return "这里是资源3,只能p3权限访问";
    }

    @PreAuthorize("hasRole('admin')")
    @RequestMapping("/r4")
    public String test4(){
        return "这里是资源4,只能admin角色访问";
    }

    @PreAuthorize("hasRole('ROLE_role1')")
    @RequestMapping("/r5")
    public String test5(){
        return "这里是资源5,只能role1角色访问";
    }

    @PreAuthorize("hasRole('role2')")
    @RequestMapping("/r6")
    public String test6(){
        return "这里是资源6,只能role2角色访问";
    }



    @PreAuthorize("isAuthenticated()")
    @RequestMapping("/r7")
    public String test7(){
        return "这里是资源7,只要认证了就能访问,认证可以是登录,也可以是RememberMe。";
    }


    @PreAuthorize("hasAnyAuthority('p3','p1')")
    @RequestMapping("/r8")
    public String test8(){
        return "这里是资源8,只要有p1或者p3权限就访问";
    }


    @PreAuthorize("hasAnyAuthority('p2') and hasRole('role2')")
    @RequestMapping("/r9")
    public String test9(){
        return "这里是资源9,只有同时拥有role2角色和p2权限才能访问";
    }


    /* 返回结果
    {"authorities":[{"authority":"ROLE_admin"},{"authority":"ROLE_role1"},{"authority":"p1"},{"authority":"p2"},{"authority":"p3"}],"details":{"remoteAddress":"0:0:0:0:0:0:0:1",
"sessionId":"4ADC66B3D98E239C9B10AC04AD43BDE0"},"authenticated":true,"principal":{"password":null,"username":"zhangsan","authorities":[{"authority":"ROLE_admin"},{"authority":"ROLE_role1"},
{"authority":"p1"},{"authority":"p2"},{"authority":"p3"}],"accountNonExpired":true,"accountNonLocked":true,"credentialsNonExpired":true,"enabled":true},"credentials":null,"name":"zhangsan"} *
*/ @PreAuthorize("isAuthenticated()") @RequestMapping("/user") public Object getLoginUser(){ Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); return authentication; } @RequestMapping("/sms/code/{phone}") public Object getSmsCode(@PathVariable("phone") String phone){ Random r = new Random(); int i = r.nextInt(10); ServletUtil.getHttpSession().setAttribute(phone,i); return i; } }

 

好了,该实现了的都实现了,但是怎么放进去呢?目前我们有一个配置spring security的类,能不能在那里面设置?答案当然是可以的

 

/**
 * 这个是spring security 的web安全配置类,这个类必须有
 * @author hongcheng
 */
@Configuration
@EnableWebSecurity      // 启动web的安全控制,这个注解好象不用也行
@EnableGlobalMethodSecurity(prePostEnabled = true)     // 启动基于方法注解的控制
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource(name="MyUserDetailsService")
    private UserDetailsService MyUserDetailsService;


    @Resource(name="SmsUserDetailsService")
    private UserDetailsService SmsUserDetailsService;

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    /**
     * 这个必须重写,才能使用AuthenticationManager,在成员变量注入进来,再注入过滤器中
     * */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        // ********************************************************************
        // 这里必须自己创建ProviderManager,并且把两个provider传进去,否则会一直创建新的ProviderManager
        // 然后不断在自己和parentAuthenticationManager之间调用,最后栈溢出。
        // 网上很多博客都是说在configure(AuthenticationManagerBuilder auth)方法中设置,我实际上测试在这个方法中设置是没用的
        ProviderManager authenticationManager = new ProviderManager(Arrays.asList(
                smsAuthenticationProvider(),daoAuthenticationProvider()
        ));
        return authenticationManager;
    }


    /**
     * 认证失败的处理器
     * */
    @Bean
    public AuthenticationFailureHandler getFailureHandler(){
        return new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                /**
                 * 认证失败的处理器
                 * 默认情况下,认证成功/失败,都是重定向到一个页面上的。
                 * 如果想要认证成功/失败后,返回信息体,而不是重定向,就必须用AuthenticationHandler
                 * 另外,绝对不要用httpServletResponse.getWriter(),一旦你用了Writer,那么setContentType("text/plain;charset=UTF-8")设置的
                 * 编码utf-8就永远不会被设置进去。具体原因自己看org.apache.catalina.connector.Response#setContentType(java.lang.String)376行
                 * */
                httpServletResponse.getOutputStream().write(e.getMessage().getBytes("UTF-8"));
                httpServletResponse.setContentType("text/plain;charset=UTF-8");
                httpServletResponse.setCharacterEncoding("UTF-8");
                httpServletResponse.flushBuffer();
            }
        };
    }

    /**
     * 认证成功的处理器,和上面一样
     * */
    @Bean
    public AuthenticationSuccessHandler getSuccessHandler(){
        return new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                httpServletResponse.getOutputStream().write("登录成功".getBytes("UTF-8"));
                httpServletResponse.setContentType("text/plain;charset=UTF-8");
                httpServletResponse.setCharacterEncoding("UTF-8");
                httpServletResponse.flushBuffer();
            }
        };
    }


    /**
     * 下面就是自定义的过滤器,配置一下拦截地址、认证成功失败处理器、authenticationManager
     * 如果还有其他认证过滤器,则再这样写一个
     * 自定义登录过滤器
     * @Author
     * @return
     */
    @Bean
    public SmsAuthenticationProcessingFilter SmsAuthenticationProcessingFilter() throws Exception {
        SmsAuthenticationProcessingFilter filter = new SmsAuthenticationProcessingFilter();
        /**
         * 自己额外添加的过滤器链必须在这里手动加上两个处理器,不然他会胡乱重定向的。
         * */
        filter.setAuthenticationSuccessHandler(getSuccessHandler());
        filter.setAuthenticationFailureHandler(getFailureHandler());
        filter.setAuthenticationManager(authenticationManager);
        return filter;
    }

    /**
     * 自定义的认证器,这是用于提供认证服务的
     * */
    @Bean
    public SmsAuthenticationProvider smsAuthenticationProvider() {
        SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
        smsAuthenticationProvider.setPasswordEncoder(noOpPasswordEncoder());
        smsAuthenticationProvider.setUserDetailsService(SmsUserDetailsService);
        return smsAuthenticationProvider;
    }


    /**
     * DaoAuthenticationProvider是给UsernamePasswordAuthenticationFilter认证用的
     * */
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(MyUserDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
        return daoAuthenticationProvider;
    }






    /**
     * 配置认证管理器
     * */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 很多博客都是在这里加上这句来添加Provider的,如果你只在这里加了,百分百执行时的
        // ProviderManager里面只有一个匿名的Provider。
        // 记住,我们要自己手动创建一个ProviderManager对象,并且创建时就加上这俩个Provider
//        auth.authenticationProvider(daoAuthenticationProvider());
//        auth.authenticationProvider(smsAuthenticationProvider());
        super.configure(auth);
    }

    /**
     * 配置url安全认证拦截的
     * */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /*
        * csrf().disable()   关闭跨域请求限制,如果开启,可能会出现很多非get请求出现405
        * .authorizeRequests()  启动请求认证
        * .antMatchers("/**").authenticated()  匹配指定的url地址进行认证判断
        * .anyRequest().permitAll()    对其他地址进行放行
        * formLogin()   启动表单登录
        * .loginPage("/login.html")    可以自定义登录页面
        * .failureUrl("/login_fail.html")   表单登录失败的跳转地址
        * .defaultSuccessUrl("/login_s.html",true)   表单登录成功的跳转地址,
        *           参数2如果为false,登录成功时会跳转到拦截前的页面,true时登录成功固定跳转给定的页面
        * .logout()   启动用户退出,security提供了默认退出地址:/logout
        * .logoutSuccessUrl("/logout.html")   成功推出后的跳转地址
        * */
        http.cors();
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/sms/login","/test/sms/code/**","/login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
//                .failureUrl("/login_fail.html")
//                .defaultSuccessUrl("/login_s.html",true)
//                .loginPage("/login.html")
//                .loginProcessingUrl("/login")
                .successHandler(getSuccessHandler())                        // 认证成功时的处理器,返回自定义信息,而不是跳转登录页
                .failureHandler(getFailureHandler())                        // 认证成功时的处理器,返回自定义信息,而不是跳转登录页
                .permitAll()
                .and()
                .logout()
                .logoutSuccessUrl("/logout.html")
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(AjaxAuthenticationEntryPoint())   // 未登录时的处理器,返回自定义信息,而不是跳转登录页面
                .accessDeniedHandler( accessDeniedHandler());               // 访问拒绝时处理器,返回自定义信息
        /**
         *  这里如果这定了页面,Handler就无效,如果是不使用spring security自带的登录页面,认证时需要始终返回json,就不要设置
         *  .failureUrl("/login_fail.html")
         *  .defaultSuccessUrl("/login_s.html",true)
         *  .loginPage("/login.html")
         *  .loginProcessingUrl("/login")
         * */

        http.addFilterBefore(SmsAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
    }



    /**
     * 一般访问资源时,如果没有权限,就会返回内置异常信息,如需要返回自定义信息,需要重写AccessDeniedHandler
     * 并需要在void configure(HttpSecurity http)方法中加
     *   .and()
     *   .exceptionHandling()
     *   .accessDeniedHandler( accessDeniedHandler());               // 访问拒绝时处理器,返回自定义信息
     * */
    @Bean
    public AccessDeniedHandler accessDeniedHandler(){
        return new AccessDeniedHandler(){
            @Override
            public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
                HashMap<String, Object> responseBody = new HashMap<>(4);

                responseBody.put("status","403");
                responseBody.put("msg",e.getMessage());
                responseBody.put("data",null);
                responseBody.put("list",null);
                httpServletResponse.setStatus(401);
                httpServletResponse.getWriter().write(responseBody.toString());
            }
        };
    }


    /**
     * 一般访问资源时,如果没有登录认证,就会跳转到登录页,重写AuthenticationEntryPoint返回自定义信息
     * 需要在void configure(HttpSecurity http)方法中加
     *   .and()
     *   .exceptionHandling()
     *   .authenticationEntryPoint(AjaxAuthenticationEntryPoint())   // 未登录时的处理器,返回自定义信息,而不是跳转登录页面
     * */
    @Bean
    public AuthenticationEntryPoint AjaxAuthenticationEntryPoint(){
        return new AuthenticationEntryPoint(){

            @Override
            public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                HashMap<String, Object> responseBody = new HashMap<>(4);

                responseBody.put("status","401");
                responseBody.put("msg","Need Authorities!");
                responseBody.put("data",null);
                responseBody.put("list",null);
                httpServletResponse.setStatus(401);
                httpServletResponse.getWriter().write(responseBody.toString());
            }
        };
    }




    /**
     * 这个是配置密码编码器的<br/>
     * BCryptPasswordEncoder表示使用BCrypt算法
     * */
    @Bean("bCryptPasswordEncoder")
    public PasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }


    /**
     * 这个是配置密码编码器的<br/>
     * NoOpPasswordEncoder表示不进行密码编码
     * */
    @Bean("noOpPasswordEncoder")
    public PasswordEncoder noOpPasswordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }


    public static void main(String[] args) {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        System.err.println(passwordEncoder.encode("123"));
    }
}

 

接下来我们就可以进行测试了。

 

首先访问我们随便访问一个资源   http://localhost:9999/test/r6

然后尝试手机号验证码登录,访问 http://localhost:9999/test/sms/code/10086 获取验证码

POST访问 http://localhost:9999/sms/login 进行登录,参数phone=10086,smsCode=上一步获取到的验证码

访问资源   http://localhost:9999/test/r1  和   http://localhost:9999/test/r6

 

访问 http://localhost:9999/logout  进行登出

 

尝试使用用户名密码进行登录 http://localhost:9999/login  ,参数username=zhangsan,password=123

 

到目前为止,多认证方式实现完毕

总结:

1:实现自定义SmsAuthenticationToken,继承 AbstractAuthenticationToken

2:实现自定义SmsAuthenticationProcessingFilter,继承 AbstractAuthenticationProcessingFilter
    指定过滤器处理的URL地址,也就是指定一个认证地址,构建一个Token,传给后续操作

3:实现自定义SmsAuthenticationProvider,继承 AbstractUserDetailsAuthenticationProvider
    指定如何处理Token,支持处理何种Token类型,如何认证用户登录是否合法

4:实现自定义SmsUserDetailsServiceImpl,实现 UserDetailsServiceImpl接口
    自定如何根据传入的参数去查询用户信息

5:重写public AuthenticationManager authenticationManagerBean()
    手动构建一个ProviderManager,将用到的AuthenticationProvider全部丢进去

6:创建一个SmsAuthenticationProcessingFilter的bean
    设置其AuthenticationManager为我们自己构建的对象

7:创建一个SmsAuthenticationProvider的bean
    设置其UserDetailsService、PasswordEncoder

8:创建一个DaoAuthenticationProvider的bean
    设置其UserDetailsService、PasswordEncoder,因为我们自己手动构建了ProviderManager,
    为了避免后续security没有自动加入默认的DaoAuthenticationProvider,我们自己手动加入

9:修改protected void configure(HttpSecurity http) 
    开放新加的认证需要使用的url,
    增加过滤器 http.addFilterBefore(SmsAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);

 

 

 

4:分布式项目怎么用spring security?

待更新

 

相关文章:

  • 2021-07-31
  • 2021-08-20
  • 2022-12-23
  • 2021-04-03
猜你喜欢
  • 2022-12-23
  • 2021-08-30
  • 2021-12-07
  • 2022-01-22
  • 2022-12-23
  • 2021-11-18
  • 2021-11-11
相关资源
相似解决方案