spring security 安全框架


编辑历史:

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

 

正文:

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

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

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

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

 

上图是网上的一些对比,如果是做单体项目,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 自动帮你创建的一个随机密码,用户名是user,登陆进去后就能正常访问我们的接口了。

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

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

 

 

 

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

 

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

@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()

 

     这是登出的,也有提供默认接口 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不仅可以将针对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的调用,所以我们一路往下找,找到最早被调用的security相关的地方

 

 

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

 

 

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

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

 

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

 

 

 

 

 

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

 

 

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

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

 

 

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

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

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

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

 

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

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

 

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

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

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

reached end of additional filter chain; proceeding with original chain

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

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

 

我们看下 VirtualFilterChain 里面都有啥

 

 

 

 

 

 

 

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

 

 

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

 

 

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

 

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

 

 

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

 

 

 

 

 

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

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

authResult = attemptAuthentication(request, response);

 

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

 

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

 

我们先看下 AuthenticationManager 是个啥玩意

 

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

 

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

 

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

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

 

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

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

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

 

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

 

 

好,继续走线程调用栈

 

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

 

 

 

 

 

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

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

 

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

 

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

 

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

 

那接下来我们看下

preAuthenticationChecks.check(user);

 

 

 

 

 

 

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

 

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

 

 

 

 现在整个方法的内容基本都看完了,整理下 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

 

 (这里我偷黑马一张图)

 

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

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
    自定义的用户查询类

 

 

 

据我们所了解的 ,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?

待更新

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM