spring sercurity教程之认证

前言

想写这篇博客很久了,但是一直就是自己心里明镜,但是要很好的总结出来,还是会有无从下手的感觉。但我还是开始写吧。一直拖 早着才能写完。

github的项目源码,建议 导入项目后,运行 然后看着博客学习。
document\urp.sql 数据库 sql

能力提升

博客只是能把你领进门,有很多特性我没有讲,感兴趣的 可以看 官方 文档
spring sercurity官方文档
如果只是满足于怎么样spring sercurity博客就可以,如果想各种定制那么还是官方文档和源码吧。

简介

谈一下 个人对spring security的理解,权限控制,这是最重要的,并且可以高度定制化,这一点学完以后大家就明白了。其次就是 spring sercurity 用了Filter和AOP。所以,大家即使不用spring sercurity 只用最简单的Filter 也是可以自己整一个权限框架出来的,至于扩展性,安全性 ,健壮性,就看你的代码水平了。

引入

我用的是spring boot项目,所以 我说一下 在spring boot项目中 ,如何引入spring sercurity。
在pom.xml文件里面

<dependencyManagement>
    <dependencies>
        
        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-bomartifactId>
            <version>5.1.6.RELEASEversion>
            <type>pomtype>
            <scope>importscope>
        dependency>
    dependencies>
dependencyManagement>

认识一些名词和熟悉流程

权限控制最主要的两大特征:

  1. 认证,即辨别你是谁
  2. 鉴权,根据认证获得的信息,判断你是否可以访问

spring sercurity的认证流程

在学习之前,我们需要记住。spring sercurity是基于Filter的。牢记这一点对你的学习很有帮助。
认证流程的主要作用:就是辨别你是谁,你的身份,你的权限等
认证流程图
Authentication:认证对象,这个会贯串整个spring sercurity流程。携带权限信息列表,密码,用户身份信息。这个会在整个流程中流动。要注意是否完全填充
spring sercurity教程之认证_第1张图片
Http Request:这个就是request请求,没啥好说的。
AuthenticationFilter: 这是spring sercurity的基础架构可以有很多个,用于提取用户名,密码,管理session。管理匿名访问等一系列的事。spring sercurity通过Filter Chain管理调用这些AuthenticationFilter:这些Filter顺序极其重要,不管你如果自定义Filter,都需要遵守下面的顺序

  1. ChannelProcessingFilter:判断是否需要重定向到另一个协议
  2. SecurityContextPersistenceFilter:在web request开始的时候,调用SecurityContextHolder设置SecurityContext,对SecurityContext的任何更改都可以在web请求结束时复制到HttpSession(准备与下一个web请求一起使用)
  3. ConcurrentSessionFilter:并发会话
  4. HeaderWriterFilter:向response添加header
  5. CsrfFilter: 关于csrf的 跨域攻击,用到了HttpSession
  6. LogoutFilter:退出系统,需要用到的,在表单登录里面常见,自己本身不处理逻辑,处理逻辑是在LogoutSuccessHandler这个接口,所以,一般自定义退出逻辑,实现LogoutSuccessHandler 注入就可以了。在HttpSecurity哪里可以设置这个。
  7. //----身份验证处理机制:从下面开始就会产生Authentication对象了
  8. X509AuthenticationFilter:关于证书的,在这里处理,是AbstractPreAuthenticatedProcessingFilter的子类
  9. CasAuthenticationFilter
  10. UsernamePasswordAuthenticationFilter:这是和平常使用最相关的一个类,主要是提取username和password 。封装成Authentication对象(没有完全填充),很重要的下面会单独细说一下。
  11. BasicAuthenticationFilter:可用于向远程协议客户机(如Hessian和SOAP)以及标准用户代理(如Internet Explorer和Netscape)提供基本身份验证服务。处理HTTP请求的基本授权头,和headers有关。
  12. RequestCacheAwareFilter:如果缓存的请求与当前请求匹配,则负责重新构造保存的请求
  13. SecurityContextHolderAwareRequestFilter:填充ServletRequest,可以和HttpServletRequestWrapper配合使用
  14. JaasApiIntegrationFilter:和Jass有关的
  15. RememberMeAuthenticationFilter:如果SecurityContext中没有Authentication,,调用RememberMeServices的实现,来生成一个Authentication到SecurityContext中。
  16. AnonymousAuthenticationFilter:如果前面没有filter中没有生成Authentication,就生成一个 匿名的Authentication 到SecurityContext。
  17. SessionManagementFilter:和Session有关的,如果request请求到这里的时候,已经被认证过就调用已配置的会话身份验证策略来执行任何与会话相关的活动,如激活会话固定保护机制或检查多个并发登录。
  18. ExceptionTranslationFilter:捕获任何Spring Security 异常,以便可以返回HTTP错误响应或使用我们定义的的AuthenticationEntryPoint返回数据
  19. FilterSecurityInterceptor:超级重要的,关于鉴权的决定了 ,用户能不能访问这个资源是AbstractSecurityInterceptor的子类,一般如果需要自定义鉴权的 继承这个类就行。在spring security的配置文件里面antMatchers 相关的 ,就是由这个Filter完成

到这里,如果 你使用上面的Filter自定义子类来替换,并且可以生成Authentication认证对象。那么认证流程就结束了。
note SecurityContextPersistenceFilter, ExceptionTranslationFilter 和FilterSecurityInterceptor 这些由 元素创建的,不能替换掉.

系统默认的一些认证流程实现类

现在说一下,spring sercurity自带的默认实现类,也就是上面图上面画的一些元素。
AuthenticationManager:这是一个接口,有一个authenticate方法,最重要的实现类是ProviderManager。这个会在上面的UsernamePasswordAuthenticationFilter里面持有Filter调用AuthenticationManager的authenticate方法,来进行身份认证。 note,但是AuthenticationManager并不执行具体的认证过称,而是由它持有的AuthenticationProvider集合来处理。只要有一个AuthenticationProvider认证通过,那么就不会再调用下面的AuthenticationProvider。

AuthenticationProvider:具体的执行认证过称的接口,有一个上面AuthenticationManager相同的方法。常用的实现类有DaoAuthenticationProvider内部持有一个下面的UserDetailsService 用于提供UserDetails.然后 也是在这里进行additionalAuthenticationChecks()密码的比对。

UserDetailsService:加载用户的数据,例如 用户名,密码 权限等,被AuthenticationProvider持有。会返回一个UserDetails。

UserDetails: 提供核心用户信息。 Spring Security不直接UserDetails将用于安全目的。它们只是存储用户信息,**这些信息稍后被封装到Authentication对象中。**这允许将与安全无关的用户信息(如电子邮件地址、电话号码等)存储在一个方便的位置。
通常 自己实现。也有系统默认的实现User。

上述流程

1Http Request 请求进入,AuthenticationFilter按照它们的顺序进行拦截其中有一个UsernamePasswordAuthenticationFilter会将1Http Request 中的userName和password 提取出来,生成一个没有完全填充的Authentication认证对象。这个认证对象随即就会被传入UsernamePasswordAuthenticationFilter的内部变量AuthenticationManager的authenticate方法AuthenticationManager会调用自己内部的AuthenticationProvider集合,对上面生成的Authentication对象 进行认证。AuthenticationProvider会调用UserDetailsServiceUserDetailsService则根据传入的userName 来取出 我们在数据库中的userName和password和权限,并把这些userName,password 和权限信息封装到生成的UserDetails里面。并且进行密码的校验。密码正确的就会生成一个完全填充的Authentication认证对象并把这个完全填充的Authentication对象放到SecurityContextHolder的SecurityContext里面。认证流程就可以结束了。下面是鉴权流程 才会开始、

例子

正如现在网站开发,分为 前后端分离,以及freemarker形式的半分离,以及JSP这种完全不分离的形式。spring sercurity可以完美支持这三种形式。
这里简单说一下在spring security中的区别:
前后端分离的一般用到token,其他的就是普通的表单登录基于session的。
我下面也分为两种分别进行讲解,虽然 其实如果你熟悉以后,会发现 差别吧 并不是很大,但是为了让新手更快速的学习,所以 需要纯粹一点。

Form表单登录

在引入spring sercurity以后,我们需要创建一个WebSecurityConfigurerAdapter的子类。FormSecurityConfig 并添加@Configuration和@EnableWebSecurity注解


/**
 * 配置form 登录的
 *
 * @Author: plani
 * 创建时间: 2019/8/16 17:50
 */
@Configuration
@EnableWebSecurity
//启用 方法鉴权  全局 只能有一个类 有这个注解。
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
@Order(3)//通过这个数字 来决定两个config那个生效
public class FormSecurityConfig extends WebSecurityConfigurerAdapter {
    private Logger logger = LoggerFactory.getLogger(FormSecurityConfig.class);
    @Autowired
    private PermissonService permissonService;

    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;

    @Autowired
    private MyAuthticationProvider myAuthticationProvider;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .csrf().disable()//关闭csrf
                .formLogin()//设置表单登录的一些细节,就是对 form表达提交  做了一些封装。完全可以自己直接请求接口
                .loginPage("/login")//用户没有登录的话,登录页面,这个需要你在controller方法里面定义这个,然后 转到
                //处理的url,这个会把filter的拦截url 改成/myaction,没有什么影响,默认是 login
                //如果 这里  你自定义了 url  那么,form 表单的action 就必须是 /myaction
                .loginProcessingUrl("/myaction")
                .usernameParameter("userName")//设定参数名字
                .passwordParameter("password")//设定参数名字
                .failureUrl("/login?error")//这个是验证密码错误,以后要跳转的页面,默认就是 /login?error
                .successForwardUrl("/index/success")//这是一个post转发请求,注意post
                .permitAll()//放开限制,允许任何人访问
                //下面的会覆盖 上面的 successForwardUrl  和 failureUrl  ,使他们不起作用。
                /*       .successHandler((request, response, authentication) -> {
                           //成功认证 以后的 处理。  可以放一些业务需要的逻辑
                           logger.info("认证成功");
                       })
                       .failureHandler((request, response, exception) -> {
                           //认证失败以后的处理
                           logger.info("认证失败");
                       })*/
                .and()
                .logout().logoutUrl("/logout")//退出 url ,默认就是 /logout
                .and()
                .authorizeRequests()//对requset 开启访问控制
                //   index/asdf   可以被任何人访问,访问这个url 如果没有登录的话,也不会跳到登录页面
                .antMatchers("/index/asdf").permitAll()
                //这个匹配器将使用与Spring MVC用于匹配的相同的规则。例如,通常路径“/path”的映射会与“/path”、“/path/”、“/path.html”匹配
                .mvcMatchers("mvc").authenticated()
                //其他的任何 url都开启 认证。 要注意静态资源
                .anyRequest().authenticated()
                .and()
                .httpBasic();//开启httpBasic

        //下面这个配置  会把上面的 loginPage覆盖掉。 form登录时候 就不要用这个了
/*        //配置 全局异常 处理方案
        httpSecurity.exceptionHandling()
                //没有权限访问 时候  返回信息
                .accessDeniedHandler(restfulAccessDeniedHandler)
                //当未登录或者token失效访问接口时,自定义的返回结果
                .authenticationEntryPoint(restAuthenticationEntryPoint);*/


        //使用自己定义的  authenticationProvider
//        httpSecurity.authenticationProvider(myAuthticationProvider);


    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth    // 设置UserDetailsService
                .userDetailsService(userDetailsService())
                // 使用BCrypt进行密码的hash
                .passwordEncoder(passwordEncoder());
//                .and()
//                .authenticationProvider(myAuthticationProvider);
    }


    // 装载BCrypt密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {//密码加密
        return new BCryptPasswordEncoder();
    }


    @Bean
    public UserDetailsService userDetailsService() {
        //获取登录用户信息
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                logger.info("username " + username);
                User user = permissonService.getUserByUserName(username);
                if (user != null) {
                    return new SecurityUser(user, permissonService);
                }
                throw new UsernameNotFoundException("没有用户");
            }
        };
    }
    
}

上面这个类中,我们 在AuthenticationManagerBuilder进行设置,配置了 自己的 userDetailsService,使用默认的PasswordEncoder。
在HttpSecurity 哪里,关闭了 开启了 form登录,这里指的注意的就是 所有的url, 前面加 " / " 比较好。还有就是 authorizeRequests后面的表达式 是按照顺序来执行的!前面的优先级最高
要注意
.successForwardUrl("/index/success") 登录成功以后 ,转发的是post请求

SecurityUser 这个类不是必须的

/**
 * @Author: plani
 * 创建时间: 2019/8/16 17:54
 */

public class SecurityUser implements UserDetails {
    private User user;

    private PermissonService permissonService;


    public SecurityUser(User user, PermissonService permissonService) {
        this.user = user;
        this.permissonService = permissonService;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<Role> roles = permissonService.rolesByUser(user);
        List<GrantedAuthority> authorities = null;
        if (roles!=null&&!roles.isEmpty()){
           authorities= roles.stream().map(new Function<Role, GrantedAuthority>() {
                @Override
                public GrantedAuthority apply(Role role) {
                    //用注解时候  hasRole  判断 角色前面 加 "ROLE_"
//                    return new SimpleGrantedAuthority("ROLE_"+role.getRoleName());
                    //hasAuthority  的时候  就不需要加任何前缀
                    return new SimpleGrantedAuthority(role.getRoleName());
                }
            }).collect(Collectors.toList());
        }
        return authorities;
    }

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

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    //账户是否未过期,过期无法验证
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    //指定用户是否解锁,锁定的用户无法进行身份验证
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    //指示是否已过期的用户的凭据(密码),过期的凭据防止认证
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //是否可用 ,禁用的用户不能身份验证
    @Override
    public boolean isEnabled() {
        return true;
    }
}

SecurityUser 并不是必须的 ,框架有自己的实现类 User。 只是为了好理解,简单的权限 可以这么使用。复杂的权限,这个就不太灵活了。

/**
 * @Author: plani
 * 创建时间: 2019/8/16 17:54
 */

public class SecurityUser implements UserDetails {
    private User user;

    private PermissonService permissonService;


    public SecurityUser(User user, PermissonService permissonService) {
        this.user = user;
        this.permissonService = permissonService;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<Role> roles = permissonService.rolesByUser(user);
        List<GrantedAuthority> authorities = null;
        if (roles!=null&&!roles.isEmpty()){
           authorities= roles.stream().map(new Function<Role, GrantedAuthority>() {
                @Override
                public GrantedAuthority apply(Role role) {
                    //用注解时候  hasRole  判断 角色前面 加 "ROLE_"
//                    return new SimpleGrantedAuthority("ROLE_"+role.getRoleName());
                    //hasAuthority  的时候  就不需要加任何前缀
                    return new SimpleGrantedAuthority(role.getRoleName());
                }
            }).collect(Collectors.toList());
        }
        return authorities;
    }

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

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    //账户是否未过期,过期无法验证
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    //指定用户是否解锁,锁定的用户无法进行身份验证
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    //指示是否已过期的用户的凭据(密码),过期的凭据防止认证
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //是否可用 ,禁用的用户不能身份验证
    @Override
    public boolean isEnabled() {
        return true;
    }
}

controller

/**
 * @Author: plani
 * 创建时间: 2019/8/19 10:30
 */
@Controller
@RequestMapping("/index/")
public class IndexControllerr {

    private Logger logger = LoggerFactory.getLogger(IndexControllerr.class);


    @RequestMapping("root")
    //要用  root 权限 才可以访问这个接口
    @PreAuthorize("hasAuthority('admin')")
    public String root(Model model) {
        logger.info("我进来了");
        //可以通过 SecurityContextHolder.getContext().getAuthentication() 来得到当前的认证对象
        //Authentication 有很多信息,你可以自己实现一个类,任何在filter中调用SecurityContextHolder.getContext().setAuthentication(); 存储你的认证对象
        //这里得到的有可能是 你自定义的,也有可能是 匿名的认证对象。
        model.addAttribute("user", SecurityContextHolder.getContext().getAuthentication().getName());

        return "index";
    }

    //不需要 任何权限都可以访问,但这个不代表 任何人都可以访问这个接口,这个需要看你的设置。
    @RequestMapping("norole")
    public String noRole(Model model) {
        return "norole";
    }

    @RequestMapping("success")
    public String success() {
        return "success";
    }
   //在配置文件里面 放开这个接口 ,也没有配置鉴权相关的注解,所以 访问这个接口没有任何限制
    @RequestMapping("asdf")
    public String asd() {
        System.out.println("ssssssssssssssssssssssss");
        return "asdf";
    }
}

上面这个里面 主要定义了, 一个 “/index/root” 路径,需要有"admin"权限。 “/index/norole” 不需要 权限,只需要 登录就行。“asdf” 这个路径 不需要登录 你就可以访问他。

这三个路径要和上面的配置文件 一起看有效,在上面 .antMatchers("/index/asdf").permitAll() 放开了 “/index/asdf” 这个路径访问 不受控制。 其他的路径,在上面的配置文件中,配置了 都需要 登录才行,
.anyRequest().authenticated() 。这段代码的意思就是 其他的任何路径,都需要认证过也就是登录状态。

登录的html


<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <meta charset="UTF-8">
    <title>Login pagetitle>
head>
<body>
<h1>Login pageh1>
<div style="width:100%;text-align:center">
    <div th:if="${param.error}">
        Invalid username and password.
    div>
    <div th:if="${param.logout}">
        You have been logged out.
    div>
    <form th:action="@{/myaction}" method="post" style="padding: auto">
        <label for="username">Usernamelabel>:
        <input type="text" id="userName" name="userName" autofocus="autofocus" style="margin-top: 10%"/> <br/>
        <label for="password">Passwordlabel>:
        <input type="password" id="password" name="password"/> <br/>
        <input type="submit" value="Login"/>
    form>
div>
body>
html>

这个就是自定义的登录页面 很简陋, 注意点 就是 action 要和 你上面.loginProcessingUrl("/myaction") 要保持一致。

运行讲解

先访问http://localhost:8888/index/asdf ,由于这个接口是放开的,所以 不登录就可以访问。
spring sercurity教程之认证_第2张图片
启动上面的项目,访问http://localhost:8888/index/root。因为这个路径 需要登录,所以 跳到了/login 页面,又因为我在ViewControllerRegistry 哪里 添加了 registry.addViewController("/login").setViewName(“login”); 把"/login"指向 login.html。
spring sercurity教程之认证_第3张图片
输入 用户名,密码。会发现 跳到 我们自定义的 登录成功界面。

spring sercurity教程之认证_第4张图片
在浏览器的控制台 也多了一个请求,我们查看一些
spring sercurity教程之认证_第5张图片
spring sercurity教程之认证_第6张图片
spring sercurity教程之认证_第7张图片
我们可以看到 提交表单后,会提交userName和password参数 到我们自定义的 路径去。并且 请求 还携带了 Cookies。 登录成功以后 返回我们自定义的成功页面。并且 返回一个Cookies。这就是为什么 form是基于session的。

再请求 需要权限的路径,
spring sercurity教程之认证_第8张图片
请求的时候 携带了 登录成功以后 返回的 cookis值。这个就相当于 是token,只不过 在form表单里面是放在cookies里面。

基于token登录

基于token登录的 spring sercurity 框架 其实已经有了 这方法的实现 ,和OAuth2 一起的,公司目前的项目 就是用的这个OAuth。这个博客 不写OAuth2.对于新手来说,一开始的时候 要,贪多嚼不烂。大道至简,才是永恒的真理
创建一个配置文件 TokenSecurityConfig .

/**
 * 配置token登录的
 *
 * @Author: plani
 * 创建时间: 2019/8/16 17:50
 */
@Configuration
@EnableWebSecurity
//启用 方法鉴权
//下面这个注解 ,项目只能有一个类可以使用,要注意
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
@Order(3)
public class TokenSecurityConfig extends WebSecurityConfigurerAdapter {
    private Logger logger = LoggerFactory.getLogger(FormSecurityConfig.class);
    @Autowired
    private PermissonService permissonService;

    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;

    @Autowired
    private MyAuthticationProvider myAuthticationProvider;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {

        httpSecurity.csrf().disable()//跨域攻击去除
                .sessionManagement()// 基于token,所以不需要session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()//返回SecurityBuilder
                .authorizeRequests()//配置url
                //这里应该考虑 放开 你的注册接口, 登录接口默认是 /login
                //这里的antMatchers  是在 Filter Chain 最后的Filter起效果。所以 不用放开 /login
                .antMatchers("/register")
                .permitAll()////允许所有人访问
                .anyRequest()//所有请求
                .authenticated();//授权的用户

        httpSecurity
                //Filter Chain 里面的UsernamePasswordAuthenticationFilter类的位置  添加  我们自定义的Filter
                //在指定Filter类的位置添加筛选器,要注意 位置,这个不要求是Sercurity的 Filter 实例
                .addFilterAt(jwtLoginFilter(), UsernamePasswordAuthenticationFilter.class)

                //在 UsernamePasswordAuthenticationFilter 的位置前面加入 Filter
//                .addFilterBefore(jwtLoginFilter(), UsernamePasswordAuthenticationFilter.class)

                //添加一个Filter,该Filter必须是安全框架中提供的Filter的实例或扩展其中一个Filter。该方法确保自动处理Filter的排序。
//                .addFilter(jwtLoginFilter())

                .addFilterAt(jwtAuthenticationFilter(), BasicAuthenticationFilter.class)
                //这个 MyFilter
                .addFilterAt(new MyFilter(), LogoutFilter.class);

        //配置 全局异常 处理方案
        httpSecurity.exceptionHandling()
                //没有权限访问 时候  返回信息
                .accessDeniedHandler(restfulAccessDeniedHandler)
                //当未登录或者token失效访问接口时,自定义的返回结果
                .authenticationEntryPoint(restAuthenticationEntryPoint);
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth    // 设置UserDetailsService
                .userDetailsService(userDetailsService)
                // 使用BCrypt进行密码的hash
                .passwordEncoder(passwordEncoder);
//                .and()
//                .authenticationProvider(myAuthticationProvider);
    }


    @Bean
    //这种用 @Bean的 并不是 唯一的,如果 你想 ,你可以直接在上面使用的地方  new一个,不过 类里面的一些其他组件 就没法注入了
    public JWTAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        return new JWTAuthenticationFilter(authenticationManager());
    }

    @Bean
    public JWTLoginFilter jwtLoginFilter() throws Exception {
        JWTLoginFilter jwtLoginFilter = new JWTLoginFilter();
        jwtLoginFilter.setAuthenticationManager(authenticationManagerBean());
        return jwtLoginFilter;
    }

}

JWTLoginFilter 登录的 生成token

校验密码成功后,将生成的token 放入header中返回

/**
 * @Author: plani
 * 创建时间: 2019/8/19 9:49
 */
//UsernamePasswordAuthenticationFilter 默认拦截 /login 路径 获取参数
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    //在这里进行 进行身份认证。这个方法 会被父类调用
    //认证失败的 直接抛出异常就行 异常必须是AuthenticationException的本身或者 子类

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        UsernamePasswordAuthenticationToken authRequest = null;
        //处理 json数据
        if (MediaType.APPLICATION_JSON_VALUE.equals(request.getContentType()) || MediaType.APPLICATION_JSON_UTF8_VALUE.equals(request.getContentType())) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                //这里使用自己熟悉的json框架都行
                JsonNode jsonNode = objectMapper.readTree(request.getInputStream());
                String username = jsonNode.get("username").asText("");
                String password = jsonNode.get("password").asText("");
                username = username.trim();
                //用于简单表示用户名和密码的身份验证}实现。
                authRequest = new UsernamePasswordAuthenticationToken(
                        username, password);
            } catch (IOException e) {
                //抛出异常 ,必须是 AuthenticationException的子类
                throw new InternalAuthenticationServiceException(e.getMessage());
            }
            //setDetails(request, authRequest) 是将当前的请求信息设置到 UsernamePasswordAuthenticationToken 中。
            setDetails(request, authRequest);
            //调用 AuthenticationManager 进行 认证。这个就是我上面说的流程了
            //你也可以在这里 就进行 密码的验证,一切看自己 可定制度很高
            return this.getAuthenticationManager().authenticate(authRequest);
        } else {
            //其他的走,父类默认的方法
            return super.attemptAuthentication(request, response);
        }
    }

    //只要上面的 attemptAuthentication没有抛出异常 ,就一定会走这里
    // 用户成功登录后,这个方法会被调用,我们在这个方法里生成token
    @Override
    protected void successfulAuthentication(HttpServletRequest req,
                                            HttpServletResponse res,
                                            FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {

        // 把完全填充的Authentication 设置 就是下面的这一行代码
        SecurityContextHolder.getContext().setAuthentication(auth);
        //把token返回  放在header中
        String token = jwtTokenUtil.generateToken(((SecurityUser) auth.getPrincipal()).getUsername());
        res.addHeader("Authorization", "Bearer" + token);
        res.getOutputStream().write("success  token".getBytes());
    }

    //这个是 登录失败,会被调用的 ,也就是上面attemptAuthentication 抛出异常 就会走这个方法
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        //这里可以自己 通过 response 写返回的信息,
        response.getOutputStream().write("error".getBytes());

        //也可以调用父类的 方法,走spring sercurity 定义好的流程 返回 ,配置文件里面定义的那些 failureHandler
        //默认实现的failureHandler 最后会指向/ 路径  ,这个要注意。
//        super.unsuccessfulAuthentication(request, response, failed);


    }
}

JWTAuthenticationFilter

丛header中 提取 token 进行 校验,通过了 就生成 完整的认证对象。放入sercurity context中。

package com.example.sercurity.config.sercurity;

import com.example.sercurity.utils.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

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

/**
 * @Author: plani
 * 创建时间: 2019/8/19 17:08
 */
//并不一定 非要用这个BasicAuthenticationFilter 作为父类,可以根据Filter的顺序 自己选择
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
    private Logger logger = LoggerFactory.getLogger(JWTAuthenticationFilter.class);
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //获取 header
        String authHeader = request.getHeader(this.tokenHeader);
        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
            //获取 token
            String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
            //提取出 名字
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            logger.info("token 提取的username   >>> ", username + "   >>>>");
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                if (jwtTokenUtil.validateToken(authToken, userDetails.getUsername())) {
                    //三个参数的构造方法  就是setAuthenticated  认证过了 ,可以看源码
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    logger.info("authenticated user:{}", username);
                    //设置到 SecurityContext里面
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        //否则的  直接走流程  调用下一个Filter
        chain.doFilter(request, response);
    }
}

运行示例

运行项目 ,打开postman 输入url 和参数
spring sercurity教程之认证_第9张图片
可以看见 header中已经 有了token,下次请求时候 把这个token放入header中就行。
在这里插入图片描述
控制台还有 这么一个 日志。这个日志 就是配置文件 里面 MyFilter类 打印的。

接下来 请求其他的接口

spring sercurity教程之认证_第10张图片
可以看到 可以访问了。

说到这里 认证这一部分 就结束 了。有的细节 我就省略了,因为 spring sercurity的知识点很杂,不可能都说完。我的代码里面 每个注释 都很重要 ,不重要的 我就不会写出来了。最重要的时候,上面的流程 一定要看懂,事半功倍

权限鉴权

也是我写的博客
spring sercurity之鉴权

你可能感兴趣的:(java开发)