首页 > 其他分享 >Spring Security认证

Spring Security认证

时间:2022-12-07 18:05:07浏览次数:43  
标签:http 登录 Spring 认证 Security throws 页面

Spring Security认证

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于Spring的应用程序的实际标准。Spring Security是一个框架,致力于为Java应用程序提供身份验证和授 权。与所有Spring项目一样,Spring Security的真正强大之处在于可以轻松扩展以满足自定义要求.

功能简介

  1. 认证: 用户登录, 解决的是"你是谁?"

  2. 授权: 判断用户拥有什么权限,可以访问什么资源. 解决的是"你能干什么?"

  3. 安全防护,防止跨站请求,session 攻击等

SpringSecurity依赖

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

测试案例

使用Spring Initializr 快速过构建Spring Boot Demo,Spring Boot版本选择2.6.8 , 并选中Spring Web 模块,编写controller如下

public class HelloSecurityController {
    @RequestMapping("/hello")
    public String hello() {
        return "hello security";
    }
}

访问 http://localhost:18080/hello

Spring Boot 已经为 Spring Security 提供了默认配置,默认所有资源都必须认证通过才能访问。那么问题来了!此刻并没有连接数据库,也并未在内存中指定认证用户,如何认证呢?

其实 Spring Boot 已经提供了默认用户名 user,密码在项目启动时随机生成,如图:

认证通过后可以继续访问处理器资源.


Spring Security 认证

网上找了一套半成品代码(有需要的可找我提供),我们用Spring Security来完善一下;

技术支持:SpringBoot 2.6.8,thymeleaf,maven

pom依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <!--添加web依赖 -->
    <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>
    <!--添加mp依赖 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.2</version>
    </dependency>
    <!--添加mysql 依赖 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.21</version>
    </dependency>
    <!--添加redis 依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>

结构层次也很简单;

SpringSecurity认证基本原理与认证2种方式

在使用 Spring Security 框架,该框架会默认自动地替我们将系统中的资源进行保护,每次访问资源的时候都必须经过一层身份的校验,如果通过了则重定向到我们输入的url中,否则访问是要被拒绝的。那 么 Spring Security 框架是如何实现的呢? Spring Security功能的实现主要是由一系列过滤器相互配合完成。也称之为过滤器链,如图:

过滤器是一种典型的AOP思想,下面简单了解下这些过滤器链,后续再源码剖析中在涉及到过滤器链在仔细讲解.

  1. org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter

    根据请求封装获取WebAsyncManager,从WebAsyncManager获取/注册的安全上下文可调
    用处理拦截器
  2. org.springframework.security.web.context.SecurityContextPersistenceFilter

    SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存
    或更新一个SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续fifilter
    建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。
  3. org.springframework.security.web.header.HeaderWriterFilter

    向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制
  4. org.springframework.security.web.csrf.CsrfFilter

    csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的
    token信息,如果不包含,则报错。起到防止csrf攻击的效果
  5. org.springframework.security.web.authentication.logout.LogoutFilter

    匹配URL为/logout的请求,实现用户退出,清除认证信息
  6. org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

    表单认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求
  7. org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter

    如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面
  8. org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter

    由此过滤器可以生产一个默认的退出登录页面
  9. org.springframework.security.web.authentication.www.BasicAuthenticationFilter

    此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息
  10. org.springframework.security.web.savedrequest.RequestCacheAwareFilter

    通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存
    HttpServletRequest
  11. org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter

    针对ServletRequest进行了一次包装,使得request具有更加丰富的API
  12. org.springframework.security.web.authentication.AnonymousAuthenticationFilter

    当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到
    SecurityContextHolder中。spring security为了兼容未登录的访问,也走了一套认证流程,
    只不过是一个匿名的身份
  13. org.springframework.security.web.session.SessionManagementFilter

    securityContextRepository限制同一用户开启多个会话的数量
  14. org.springframework.security.web.access.ExceptionTranslationFilter

    异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异
    常
  15. org.springframework.security.web.access.intercept.FilterSecurityIntercepto

    获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其
    是否有权限

Spring Security默认加载15个过滤器, 但是随着配置可以增加或者删除一些过滤器.

认证方式

认证方式有两种,一种是HttpBasic认证,另外一种是formLogin登录认证模式

HttpBasic认证

HttpBasic登录验证模式是Spring Security实现登录验证最简单的一种方式,也可以说是最简陋的一种方式。它的目的并不是保障登录验证的绝对安全,而是提供一种“防君子不防小人”的登录验证;HttpBasic模式要求传输的用户名密码使用Base64模式进行加密。如果用户名是 "admin" , 密码是“ admin”,则将字符串"admin:admin" 使用Base64编码算法加密。加密结果可能是: YWtaW46YWRtaW4=。HttpBasic模式真的是非常简单又简陋的验证模式,Base64的加密算法是可逆的,想要破解并不难.

formLogin登录认证模式

通过携带Http的Header进行 简单的登录验证,而且没有定制的登录页面,所以使用场景比较窄。对于一个完整的应用系统,与 登录验证相关的页面都是高度定制化的,非常美观而且提供多种登录方式。这就需要Spring Security支持我们自己定制登录页面, spring boot2.0以上版本(依赖Security 5.X版本)默认会生 成一个登录页面.


表单认证

下面我们根据半成品代码自定义一个表单认证(由于HttpBasic认证不安全,所以不予深究...)

在config包下编写SecurityConfiguration配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
    /**
     * http请求方法
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
/*        http.httpBasic() //开启httpBasic认证
                .and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
​
        http.formLogin().loginPage("/login.html")//开启表单认证
                .and().authorizeRequests() //放行登录页面
                .anyRequest().authenticated();
​
    }

注意:WebSecurityConfigurerAdapter 抽象类有三个configure方法,分别对应的是 身份安全管理器, 忽略静态资源, http请求方法;

点击运行发现如下问题

因为设置登录页面为login.html 后面配置的.anyRequest().authenticated()是所有请求都登录认证,陷入了死循环. 所以需要将 login.html放行不需要登录认证

这边修改一下代码

@Override
    protected void configure(HttpSecurity http) throws Exception {
/*        http.httpBasic() //开启httpBasic认证
                .and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
​
        http.formLogin().loginPage("/login.html") //开启表单认证
                .and().authorizeRequests().antMatchers("/login.html").permitAll() //放行登录页面
                .anyRequest().authenticated();
​
    }

点击重新测试如下

提示404了。这是怎么回事?

该工程页面已经整合了 thymeleaf ,那我们直接访问login.html就不行了,他需要请求之后然后跳转到该页面,那我们就写个方法让他跳转到该页面呗;

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

然后我们再修改一下SecurityConfig

 @Override
    protected void configure(HttpSecurity http) throws Exception {
/*        http.httpBasic() //开启httpBasic认证
                .and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
​
        http.formLogin().loginPage("/toLoginPage") //开启表单认证
                .and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
                .anyRequest().authenticated();
​
    }

重新启动运行,结果如下

登录是正常了,只不过样式没有了,估计是被security拦截掉了,打开控制台显示css,images,js都302了;

SecurityConfig 继承 WebSecurityConfigurerAdapter后,有三个 configure 方法,其中有一个是 configure(WebSecurity web),目测是web相关的东西,如下

@Override
public void configure(WebSecurity web) throws Exception {
    //解决静态资源被拦截的问题
    web.ignoring().antMatchers("/css/**","/images/**","/js/**");
}

忽略掉和静态资源相关的东西,继续访问如下:

霍,没问题了!

Spring Security 中,安全构建器 HttpSecurity 和 WebSecurity 的区别是 :

  1. WebSecurity 不仅通过 HttpSecurity 定义某些请求的安全控制,也通过其他方式定义其他某些 请求可以忽略安全控制;

  2. HttpSecurity 仅用于定义需要安全控制的请求(当然 HttpSecurity 也可以指定某些请求不需要 安全控制);

  3. 可以认为 HttpSecurity 是 WebSecurity 的一部分, WebSecurity 是包含 HttpSecurity 的更大 的一个概念;

  4. 构建目标不同

    1. WebSecurity 构建目标是整个 Spring Security 安全过滤器 FilterChainProxy

    2. HttpSecurity 的构建目标仅仅是 FilterChainProxy 中的一个 SecurityFilterChain


表单登录

页面问题解决了,下面我们来解决一下表单登陆的问题,上面15个过滤器链有个UsernamePasswordAuthenticationFilter是处理表单登录的;打开源码看一下都有些啥子。

在源码中可以观察到, 表单中的input的name值是 username 和 password, 并且表单提交的路径 为 /login , 表单提交方式method为 post ,

这些可以修改为自定义的值.

修改一下config配置类以及login页面

    @Override
    protected void configure(HttpSecurity http) throws Exception {
​
/*        http.httpBasic() //开启httpBasic认证
                .and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
​
        http.formLogin() //开启表单认证
                .loginPage("/toLoginPage") // 自定义登陆页面
                .loginProcessingUrl("/login") //表单提交路径
                .usernameParameter("username").passwordParameter("password") //自定义input额name值和password
                .successForwardUrl("/") //登录成功之后跳转的路径
                .and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
                .anyRequest().authenticated()
                .and().csrf().disable();//关闭csrf防护
    }

login.html

代码修改后重启完成登录

这时候又出现了新的坑,

index.html

发现行内框架 iframe 这里出现问题了. Spring Security下,X-Frame-Options默认为DENY,非Spring Security环境下,X-Frame-Options的默认大多也是DENY,这种情况下,浏览器拒绝当前页面加载任何 Frame页面,设置含义如下:

  • DENY:浏览器拒绝当前页面加载任何Frame页面 此选择是默认的.

  • SAMEORIGIN:frame页面的地址只能为同源域名下的页面

允许iframe加载

 @Override
    protected void configure(HttpSecurity http) throws Exception {
​
/*        http.httpBasic() //开启httpBasic认证
                .and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
​
        http.formLogin() //开启表单认证
                .loginPage("/toLoginPage") // 自定义登陆页面
                .loginProcessingUrl("/login") //表单提交路径
                .usernameParameter("username").passwordParameter("password") //自定义input额name值和password
                .successForwardUrl("/") //登录成功之后跳转的路径
                .and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
                .anyRequest().authenticated()
                .and().headers().frameOptions().sameOrigin() //加载同源域名下iframe页面
                .and().csrf().disable();//关闭csrf防护
​
    }

修改之后再次登录如下:

完成!


基于数据库实现认证功能

之前我们所使用的用户名和密码是来源于框架自动生成的, 现在我们需要实现基于数据库中的用户名和密码功能,首先得需要实现security的一个UserDetailsService 接口, 重写这个接口里面 loadUserByUsername

@Service
public class MyUserDetailsService implements UserDetailsService {
​
    @Autowired
    private UserService userService;
​
    /**
     * 根据用户名查询用户
     * @param username 前端传入的用户名
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("user is ==>" + username);
        }
        Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
        UserDetails userDetails = new org.springframework.security.core.userdetails.User(username,
                                                                        "{noop}"+user.getPassword(),//不使用密码加密
                                                                        true, //用户是否启用 (true:启用)
                                                                        true,//用户是否过期 (true:没有过期)
                                                                        true, //用户凭证是否过期 (true:没有过期)
                                                                        true, // 用户是否锁定 (true:没有锁定)
                authorities);
        return userDetails;
    }
}

在SecurityConfiguration配置类中指定自定义用户认证

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
    @Autowired
    private MyUserDetailsService myUserDetailsService;
​
    /**
     *身份安全管理器
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService);
    }
}

此时,控制台也不会生成随机密码,现在走的是数据库的认证方式;


密码加密认证

在基于数据库完成用户登录的过程中,我们所是使用的密码是明文的,规则是通过对密码明文添加 {noop} 前缀。那么下面 Spring Security 中的密码编码进行一些探讨。

Spring Security 中 PasswordEncoder 就是我们对密码进行编码的工具接口。该接口只有两个功能: 一个是匹配验证(matches)。另一个是密码加密(encode)。

Spring Security主流用法就是 BCryptPasswordEncoder

BCrypt算法介绍:

任何应用考虑到安全,绝不能明文的方式保存密码。密码应该通过哈希算法进行加密。 有很多标准的算法比如SHA或者MD5,结合salt(盐)是一个不错的选择。Spring Security 提供了 BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码。BCrypt强哈希方法 每次加密的结果都不一样,所以更加的安全。

bcrypt加密后的字符串形如:$2a$10$wouq9P/HNgvYj2jKtUN8rOJJNRVCWvn1XoWy55N3sCkEHZPo3lyWq

其中$是分割符,无意义;2a是bcrypt加密版本号;10是const的值;而后的前22位是salt值;再然后的字符串就是密码的密文了;这里的const值即生成salt的迭代次数,默认值是10,推荐值 12。

下面在项目中使用BCrypt算法进行加密

首先先看一个 PasswordEncoderFactories,它是密码器工厂

public static PasswordEncoder createDelegatingPasswordEncoder() {
   String encodingId = "bcrypt";
   Map<String, PasswordEncoder> encoders = new HashMap<>();
   encoders.put(encodingId, new BCryptPasswordEncoder());
   encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
   encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
   encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
   encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
   encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
   encoders.put("scrypt", new SCryptPasswordEncoder());
   encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
   encoders.put("SHA-256",
         new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
   encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
   encoders.put("argon2", new Argon2PasswordEncoder());
   return new DelegatingPasswordEncoder(encodingId, encoders);
}

之前我们在项目中密码使用的是明文的是 noop , 代表不加密使用明文密码, 现在用BCrypt只需要将 noop 换成 bcrypt 即可,如下

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userService.findByUsername(username);
    if (user == null) {
        throw new UsernameNotFoundException("user is ==>" + username);
    }
    Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
    UserDetails userDetails = new org.springframework.security.core.userdetails.User(username,
                                                                    "{bcrypt}"+user.getPassword(),//使用密码加密
                                                                    true, //用户是否启用 (true:启用)
                                                                    true,//用户是否过期 (true:没有过期)
                                                                    true, //用户凭证是否过期 (true:没有过期)
                                                                    true, // 用户是否锁定 (true:没有锁定)
            authorities);
    return userDetails;
}

我们可以写个main方法测试一下

public static void main(String[] args) {
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    String encode = bCryptPasswordEncoder.encode("123456");
    System.out.println(encode);
    String encode1 = bCryptPasswordEncoder.encode("123456");
    System.out.println(encode1);
}

测试结果

$2a$10$b42Oiv0L4spIBVA5NBI9U.o7DyLFfKCU46cvzdvYo8g/MTo6lwnwi
$2a$10$sMsUp6McNkscco2yTGwWOOpnL1Jgd0sj2nxU.3aaF6kHV0jUARQDq
​
Process finished with exit code 0
​

由于BCrypt是强哈希算法,可见加密结果是不一样的;


获取当前登录用户

传统web系统中,我们将登陆成功的用户放入session中,在需要的时候,可以从session中获取用户,那么spring Security中我们可以从以下两个类获得当前已经登录的用户

  • SecurityContextHolder

    保留系统当前的安全上下文 SecurityContext,其中就包括当前使用系统的用户信息

  • SecurityContext

    安全上下文,获取当前经过身份验证的主体或身份验证请求令牌

代码实现如下(security获取当前登录用户的三种方式):

    /**
     * 获取当前登录用户
     * @return
     */
    @GetMapping("/loginUser1")
    public UserDetails getCurrnetUser1(Authentication authentication){
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        return userDetails;
    }
​
    /**
     * 获取当前登录用户
     * @return
     */
    @GetMapping("/loginUser2")
    public UserDetails getCurrnetUser2(){
        UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return userDetails;
    }
​
    /**
     * 获取当前登录用户
     * @return
     */
    @GetMapping("/loginUser3")
    public UserDetails getCurrnetUser3(@AuthenticationPrincipal UserDetails userDetails){
        return userDetails;
    }

登录之后直接访问接口,测试结果如下


remember me功能

在大多数网站中,都会实现 Remember Me 这个功能,方便用户在下一次登录时直接登录,避免再次输入用户名以及密码去登录,Spring Security针对这个功能已经帮助我们实现,如下图:

Token=MD5(username+分隔符+expiryTime+分隔符+password)

注意: 这种方式不推荐使用, 有严重的安全问题. 就是密码信息在前端浏览器cookie中存放. 如果cookie 被盗取很容易破解.

代码实现:

protected void configure(HttpSecurity http) throws Exception {
​
/*        http.httpBasic() //开启httpBasic认证
                .and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
​
        http.formLogin() //开启表单认证
                .loginPage("/toLoginPage") // 自定义登陆页面
                .loginProcessingUrl("/login") //表单提交路径
                .usernameParameter("username").passwordParameter("password") //自定义input额name值和password
                .successForwardUrl("/") //登录成功之后跳转的路径
                .and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
                .anyRequest().authenticated()
                .and().rememberMe() //开启记住我功能
                .tokenValiditySeconds(1209600) //token失效时间,默认失效时间是两周
                .rememberMeParameter("remember-me") // 自定义表单name值
                .and().headers().frameOptions().sameOrigin() //加载同源域名下iframe页面
                .and().csrf().disable();//关闭csrf防护
    }

login.html

测试如下

上面所说,密码信息也会存放在cookie中,容易被抓取破解,下面我们可以改造一下,基于持久化的Token生成方法,如下图

存入数据库Token包含:

  • token: 随机生成策略,每次访问都会重新生成

  • series: 登录序列号,随机生成策略。用户输入用户名和密码登录时,该值重新生成。使用 remember-me功能,该值保持不变

  • expiryTime: token过期时间

CookieValue=encode(series+token)

代码实现

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
    @Autowired
    private MyUserDetailsService myUserDetailsService;
​
    /**
     *身份安全管理器
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService);
    }
​
    /**
     * 忽略静态资源
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        //解决静态资源被拦截的问题
        web.ignoring().antMatchers("/css/**","/images/**","/js/**");
    }
​
    /**
     * http请求方法
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
​
/*        http.httpBasic() //开启httpBasic认证
                .and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
​
        http.formLogin() //开启表单认证
                .loginPage("/toLoginPage") // 自定义登陆页面
                .loginProcessingUrl("/login") //表单提交路径
                .usernameParameter("username").passwordParameter("password") //自定义input额name值和password
                .successForwardUrl("/") //登录成功之后跳转的路径
                .and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
                .anyRequest().authenticated()
                .and().rememberMe() //开启记住我功能
                .tokenValiditySeconds(1209600) //token失效时间,默认失效时间是两周
                .rememberMeParameter("remember-me") // 自定义表单name值
                .tokenRepository(getPersistentTokenRepository()) //设置PersistentTokenRepository
                .and().headers().frameOptions().sameOrigin() //加载同源域名下iframe页面
                .and().csrf().disable();//关闭csrf防护
    }
​
    @Autowired
    DataSource dataSource;
​
    /**
     * 负责token与数据库之间的操作
     * @return
     */
    @Bean
    public PersistentTokenRepository getPersistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource); //设置数据源
        tokenRepository.setCreateTableOnStartup(true); //启动时帮助我们自动创建一张表,第一次启动设置为true,第二次启动程序的时候设置false或者注释掉;
        return tokenRepository;
    }
​
}

运行后,数据库会生成一张对应的表

登陆成功之后,观察数据库,会插入一条记录.说明持久化token方式已经生效;

cookie窃取伪造登录成功,访问接口获取数据,只需在关键接口处添加如下代码,可解决此问题。如下:

@GetMapping("/{id}")
public User getById(@PathVariable Integer id) {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    //如果返回true,代表这个登录认证的信息来源于自动登录
    if(RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass())){
        throw new RememberMeAuthenticationException("认证来源于remember me");
    }
    User user = userService.getById(id);
    return user;
}

自定义登录成功处理和失败处理

在某些场景下,用户登录成功或者数失败的情况下用户需要执行一些后续操作,比如登录日志搜集,或者在现在目前前后端分离的情况下,用户登录成功和失败后需要给前台页面返回对应的错误信息,有前台主导成功或者失败的页面跳转,这个时候需要用到 AuthenticationSucessHandleAnthenticationFailureHandle

默认的成功和失败处理逻辑源自于下图:

进入源码查看

  • sucess

  • faile

他们分别实现了 AuthenticationSucessHandle 接口和 AnthenticationFailureHandle 接口

自定义成功处理

   实现 AuthenticationSucessHandle 接口,并重写 onAnthenticationSucess()方法;

自定义失败处理

  实现 AnthenticationFailureHandle 接口,并重写 onAnthenticationFailure() 方法

代码实现:

@Service
public class MyAuthenticationService implements AuthenticationSuccessHandler,AuthenticationFailureHandler {
​
    RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
​
    /**
     * 登录成功后的处理逻辑
     * @param request
     * @param response
     * @param authentication
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("登录成功后继续处理。。。。。。。。");
        //重定向到index。html
        redirectStrategy.sendRedirect(request,response,"/");
    }
​
    /**
     * 登录失败后的处理逻辑
     * @param request
     * @param response
     * @param exception
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.out.println("登录失败后继续处理。。。。。。。。");
        redirectStrategy.sendRedirect(request,response,"/toLoginPage");
    }
​
}
@Autowired
private MyAuthenticationService myAuthenticationService;
​
@Override
    protected void configure(HttpSecurity http) throws Exception {
​
/*        http.httpBasic() //开启httpBasic认证
                .and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
​
        http.formLogin() //开启表单认证
                .loginPage("/toLoginPage") // 自定义登陆页面
                .loginProcessingUrl("/login") //表单提交路径
                .usernameParameter("username").passwordParameter("password") //自定义input额name值和password
                .successForwardUrl("/") //登录成功之后跳转的路径
                .successHandler(myAuthenticationService) // 登录成功处理
                .failureHandler(myAuthenticationService) //登录失败处理
                .and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
                .anyRequest().authenticated()
                .and().rememberMe() //开启记住我功能
                .tokenValiditySeconds(1209600) //token失效时间,默认失效时间是两周
                .rememberMeParameter("remember-me") // 自定义表单name值
                .tokenRepository(getPersistentTokenRepository()) //设置PersistentTokenRepository
                .and().headers().frameOptions().sameOrigin() //加载同源域名下iframe页面
                .and().csrf().disable();//关闭csrf防护
    }

测试结果(失败和成功分别演示一次)


退出登录

org.springframework.security.web.authentication.logout.LogoutFilter
​
匹配URL为/logout的请求,实现用户退出,清除认证信息。

只需要发送请求,请求路径为/logout即可, 当然这个路径也可以自行在配置类中自行指定, 同时退出 操作也有对应的自定义处理LogoutSuccessHandler,退出登录成功后执行,退出的同时如果有 remember-me的数据,同时一并删除

看一下 LogoutFilter 源码:

public class LogoutFilter extends GenericFilterBean {
​
   private RequestMatcher logoutRequestMatcher;
​
   private final LogoutHandler handler;
​
   private final LogoutSuccessHandler logoutSuccessHandler;
​
   /**
    * Constructor which takes a <tt>LogoutSuccessHandler</tt> instance to determine the
    * target destination after logging out. The list of <tt>LogoutHandler</tt>s are
    * intended to perform the actual logout functionality (such as clearing the security
    * context, invalidating the session, etc.).
    */
   public LogoutFilter(LogoutSuccessHandler logoutSuccessHandler, LogoutHandler... handlers) {
      this.handler = new CompositeLogoutHandler(handlers);
      Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null");
      this.logoutSuccessHandler = logoutSuccessHandler;
      setFilterProcessesUrl("/logout");
   }
​
   public LogoutFilter(String logoutSuccessUrl, LogoutHandler... handlers) {
      this.handler = new CompositeLogoutHandler(handlers);
      Assert.isTrue(!StringUtils.hasLength(logoutSuccessUrl) || UrlUtils.isValidRedirectUrl(logoutSuccessUrl),
            () -> logoutSuccessUrl + " isn't a valid redirect URL");
      SimpleUrlLogoutSuccessHandler urlLogoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
      if (StringUtils.hasText(logoutSuccessUrl)) {
         urlLogoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl);
      }
      this.logoutSuccessHandler = urlLogoutSuccessHandler;
      setFilterProcessesUrl("/logout");
   }
​
   @Override
   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
         throws IOException, ServletException {
      doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
   }
​
   private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
         throws IOException, ServletException {
      if (requiresLogout(request, response)) {
         Authentication auth = SecurityContextHolder.getContext().getAuthentication();
         if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Logging out [%s]", auth));
         }
         this.handler.logout(request, response, auth);
         this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
         return;
      }
      chain.doFilter(request, response);
   }
​
   /**
    * Allow subclasses to modify when a logout should take place.
    * @param request the request
    * @param response the response
    * @return <code>true</code> if logout should occur, <code>false</code> otherwise
    */
   protected boolean requiresLogout(HttpServletRequest request, HttpServletResponse response) {
      if (this.logoutRequestMatcher.matches(request)) {
         return true;
      }
      if (this.logger.isTraceEnabled()) {
         this.logger.trace(LogMessage.format("Did not match request to %s", this.logoutRequestMatcher));
      }
      return false;
   }
​
   public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
      Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
      this.logoutRequestMatcher = logoutRequestMatcher;
   }
​
   public void setFilterProcessesUrl(String filterProcessesUrl) {
      this.logoutRequestMatcher = new AntPathRequestMatcher(filterProcessesUrl);
   }
​
}

默认退出路径 setFilterProcessesUrl("/logout");

发送 "/logout" 之后,退出完成之后没有指定的情况下会默认跳转到登录页面,如下源码:

代码实现如下:

index.html

<div class="header bg-main">
    <div class="logo margin-big-left fadein-top">
        <h1>后台管理中心</h1>
    </div>
    <div class="head-l">
        <a class="button button-little bg-red" href="/logout">
            <span class="icon-power-off"></span>退出登录</a></div>
</div>

后端代码 SecurityConfig:

 @Override
    protected void configure(HttpSecurity http) throws Exception {
​
/*        http.httpBasic() //开启httpBasic认证
                .and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
​
        http.formLogin() //开启表单认证
                .loginPage("/toLoginPage") // 自定义登陆页面
                .loginProcessingUrl("/login") //表单提交路径
                .usernameParameter("username").passwordParameter("password") //自定义input额name值和password
                .successForwardUrl("/") //登录成功之后跳转的路径
                .successHandler(myAuthenticationService) // 登录成功处理
                .failureHandler(myAuthenticationService) //登录失败处理
                .and().logout().logoutUrl("/logout") //退出
                .logoutSuccessHandler(myAuthenticationService) //退出后处理
                .and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
                .anyRequest().authenticated()
                .and().rememberMe() //开启记住我功能
                .tokenValiditySeconds(1209600) //token失效时间,默认失效时间是两周
                .rememberMeParameter("remember-me") // 自定义表单name值
                .tokenRepository(getPersistentTokenRepository()) //设置PersistentTokenRepository
                .and().headers().frameOptions().sameOrigin() //加载同源域名下iframe页面
                .and().csrf().disable();//关闭csrf防护
    }
@Service
public class MyAuthenticationService implements AuthenticationSuccessHandler,AuthenticationFailureHandler, LogoutSuccessHandler {
​
    RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
​
    @Autowired
    ObjectMapper objectMapper;
​
    /**
     * 登录成功后的处理逻辑
     * @param request
     * @param response
     * @param authentication
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("登录成功后继续处理。。。。。。。。");
        //重定向到index。html
        redirectStrategy.sendRedirect(request,response,"/");
/*
        Map result = new HashMap();
        result.put("code", HttpStatus.OK.value()); //200
        result.put("message","登录成功");
​
        response.setContentType("application/json;charset=UTF-8");
        //响应
        response.getWriter().write(objectMapper.writeValueAsString(result));
*/
​
    }
​
    /**
     * 登录失败后的处理逻辑
     * @param request
     * @param response
     * @param exception
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.out.println("登录失败后继续处理。。。。。。。。");
        redirectStrategy.sendRedirect(request,response,"/toLoginPage");
/*        Map result = new HashMap();
        result.put("code", HttpStatus.UNAUTHORIZED.value()); //401
        result.put("message","登录失败");
​
        response.setContentType("application/json;charset=UTF-8");
        //响应
        response.getWriter().write(objectMapper.writeValueAsString(result));*/
    }
​
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("退出之后继续处理。。。。");
        redirectStrategy.sendRedirect(request,response,"/toLoginPage");
    }
}

测试结果

标签:http,登录,Spring,认证,Security,throws,页面
From: https://www.cnblogs.com/wangshaoyun/p/16963838.html

相关文章

  • spring mvc环境之引入spring容器(七)
    springmvc环境之引入spring容器实现对项目bean的依赖注入、控制翻转等因为之前pom.xml引入了spring-web,它本身就要依赖于核心包------------然后在web.xml配置监听器,......
  • springboot不能识别controller中的接口地址原因汇总
    之前写一个模块的接口,定义好测通没问题发给同事,中间隔了两天加了些内容又测突然就报404找不到。在别的模块controller测试都能测通唯独这个测不通,见了鬼了。百度了好多原......
  • springboot+Elasticsearch 复杂查询
    以前没做过ES里面的查询,第一次接触还是走了点弯路的。就是这个字段你在ES都不用模糊查的话,就可以设置 type=FieldType.Keyword,比如ID之类的。一:建ES存储的实体imp......
  • springboot_03
    1.yam文件书写格式 1.1字面值的表示方式 1.2数组的表示方法 2.yml文件的读取   代码实际随便写个测试@RestController@RequestMapping("/books")publ......
  • SpringBoot使用maven打jar包配置
    在pom.xml文件中加入依赖<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>选择你自己的版本</ve......
  • SpringCloud-Hystrix (熔断、降级、监控)
    Hystrix:服务熔断分布式系统面临的问题复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免失败!1服务雪崩​ 多个微服务之间调用的时......
  • Spring面试
    0. 讲一下什么是SpringSpring是一个轻量级的IoC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需......
  • Spring Cloud 面试
    0.介绍1.架构2.对比3.负载均衡4.注册中心5.配置中心 6.熔断降级7. SpringCloudConfig8. SpringCloudGateway什么是网关?网关相当于一个网络服务架构的入口,所......
  • spring-boot构建docker镜像上传仓库
    spring-boot构建docker镜像上传仓库创建一个简单spring-boot-web项目<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xm......
  • SpringBoot pom.xml配置文件详解
    <?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   xsi:sch......