SpringSecurity源码理解及使用(一)

学习视频来自B站 https://space.bilibili.com/352224540?from=search&seid=6131155319413568885&spm_id_from=333.337.0.0

springSecurity 简介

原名 Acegi Security 、一款安全框架 ,类似于shiro 帮助完成登录校验,退出登录,请求拦截,权限控制等。
springSecurity 是一种应用于spring框架环境下的 不能像shiro一样用作javase
springSerutrity 高扩展性 、更多功能 更适合springCould微服务环境 、更好的实现spring全家桶环境
缺点:复杂、繁琐 结合springBoot使一些配置默认化 简化一些
源码特点:用接口搭建框架规范,用接口实现类实现功能

认证与授权

认证:判断是否是合法用户
授权:判断合法用户能够访问哪些资源 先认证再授权

整体架构

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

ChannelProcessingFilter过滤请求协议 HTTP 、HTTPSNO
过滤器过滤器作用默认是否加载
WebAsyncManagerIntegrationFilter将 WebAsyncManger 与 SpringSecurity 上下文进行集成YES
SecurityContextPersistenceFilter在处理请求之前,将安全信息加载到 SecurityContextHolder 中YES
HeaderWriterFilter处理头信息加入响应中YES
CorsFilter处理跨域问题NO
CsrfFilter处理 CSRF 攻击YES
LogoutFilter处理注销登录YES
OAuth2AuthorizationRequestRedirectFilter处理 OAuth2 认证重定向NO
Saml2WebSsoAuthenticationRequestFilter处理 SAML 认证NO
X509AuthenticationFilter处理 X509 认证NO
AbstractPreAuthenticatedProcessingFilter处理预认证问题NO
CasAuthenticationFilter处理 CAS 单点登录NO
OAuth2LoginAuthenticationFilter处理 OAuth2 认证NO
Saml2WebSsoAuthenticationFilter处理 SAML 认证NO
UsernamePasswordAuthenticationFilter处理表单登录YES
OpenIDAuthenticationFilter处理 OpenID 认证NO
DefaultLoginPageGeneratingFilter配置默认登录页面YES
DefaultLogoutPageGeneratingFilter配置默认注销页面YES
ConcurrentSessionFilter处理 Session 有效期NO
DigestAuthenticationFilter处理 HTTP 摘要认证NO
BearerTokenAuthenticationFilter处理 OAuth2 认证的 Access TokenNO
BasicAuthenticationFilter处理 HttpBasic 登录YES
RequestCacheAwareFilter处理请求缓存YES
SecurityContextHolder<br />AwareRequestFilter包装原始请求YES
JaasApiIntegrationFilter处理 JAAS 认证NO
RememberMeAuthenticationFilter处理 RememberMe 登录NO
AnonymousAuthenticationFilter配置匿名认证YES
OAuth2AuthorizationCodeGrantFilter处理OAuth2认证中授权码NO
SessionManagementFilter处理 session 并发问题YES
ExceptionTranslationFilter处理认证/授权中的异常YES
FilterSecurityInterceptor处理授权相关YES
SwitchUserFilter处理账户切换NO

默认配置原理

springsecurity 是在springBoot基础上使用
导入依赖

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

引入依赖后 由于springboot的有关security的默认配置激活 ,会将所有接口保护 弹出默认的登录页面
在这里插入图片描述
在这里插入图片描述

springBoot对SpringSecurity的配置

配置类 SpringBootWebSecurityConfiguration
在这里插入图片描述
在这里插入图片描述
登录页面位置 DefaultLoginPageGeneratingFilter 拦截器
在doFilter()方法中 判断请求是否认证成功,没有则调用方法以拼接String形成页面的方式进行返回在这里插入图片描述
在这里插入图片描述

默认数据源存放位置

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

结论 :默认数据源来自了 实现类UserDetailsService接口的InMemoryUserDetailsManger类中 。InMemoryUserDetailsManger作为默认数据源

自定义类 实现UserDetailsService接口 ,可更换数据源
在这里插入图片描述
任意继承其中一个 注册进容器 使得默认内存数据源失效

AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class

默认用户信息位置

在这里插入图片描述
配置文件中修改
在这里插入图片描述
认证流程
在这里插入图片描述
在这里插入图片描述

自定义springSecurity配置 实现 WebSecurityConfigurerAdapter接口 覆盖默认配置
在这里插入图片描述
自定义数据源的配置 实现 UserDetailService ,使得默认数据源失效
在这里插入图片描述

自定义配置

自定义资源权限规则

对不同资源进行 不同认证方式
根据源码 使默认配置失效 要实现 WebSecurityConfigurerAdapter.classSecurityFilterChain.class 通常选第一个
功能多 如图
在这里插入图片描述
默认springSecurity配置的失效条件
在这里插入图片描述
配置方式
在这里插入图片描述

自定义登录页面

默认登录页面 是在DefaultLoginPageGeneratingFilter 类中 通过字符串拼接返回显示的

<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h1>用户登录</h1>
<form method="post" th:action="">
    用户名:<input name="username" type="text"/><br>
    密码:<input name="passwrod" type="password"/><br>
    <input type="submit" value="登录"/>
</form>
</body>
</html>

自定义thymealf 页面 作为登录页面

  1. 引入thymealf模板,创建登录页面
 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
  </dependency>
  1. 由于页面受保护,前端访问需要controller 跳转 创建controller
@Controller
public class LoginController {

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

  1. 设置拦截规则 要将 登录请求放行
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()
                .mvcMatchers("/login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
        ;
    }
}
  1. 配置springSecurity 设置自定义的登录页面
       在htpp请求后加上  .loginPage("/login.html")

目前为止 ,在springSecurity 拦截请求会显示自定义的登录页面 但页面没有认证功能
在这里插入图片描述
根据源码可知,页面表单的验证请求要为POST且 参数名为username和password,登录请求要符合配置在这里插入图片描述
5. 重新修改表单 改为符合默认规则

  1. 一但自定义了登录页面,就要指定登录请求路径
.loginPage("/login.html") 后加上
	.loginProcessingUrl("/doLogin")

在这里插入图片描述
设置表单参数名

在这里插入图片描述

自定义登录成功后页面的跳转(传统web项目)

设置登录后页面显示
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

自定义登录成功后返回的json(前后端分离)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        HashMap<String, Object> map = new HashMap<>();

        map.put("msg","登录成功");
        map.put("status",200) ;
        map.put("authentication",authentication);
        response.setContentType("application/json;charset=UTF-8");
        String s = new ObjectMapper().writeValueAsString(map);
        response.getWriter().println(s);
    }
}

在这里插入图片描述
效果
登录成功 ,不在显示 页面而是显示自定义返回的json 适用于前后端分离
在这里插入图片描述

自定义显示登录失败信息

默认登录失败会重新回到登录页面
在这里插入图片描述
在处理异常时 ,将错误信息保存
在这里插入图片描述

在这里插入图片描述
由源码可知 无论放在session中还是request中 作用域的名都为 SPRING_SECURITY_LAST_EXCEPTION
由此 前端页面可根据是 redirect还是 forward 从对应作用域中获取错误信息

自定义登录失败页面跳转(传统web开发)

通登录成功跳转,失败跳转也分为重定向和转发 。通常重定向到登录页面

failureUrl(“zz”)测试

在这里插入图片描述
由于使用重定向 从request中获取则失败
在这里插入图片描述
failureForwardUrl(“zz”) 测试
在这里插入图片描述
测试发现 .failureUrl(“/err”) 错误时并未重定向到指定页面 仍是重定向到自己的登录页面
通常使用重定向到登录页面,因为重定向路径会改变

自定义登录失败后返回的json(前后端分离)

在这里插入图片描述
在这里插入图片描述

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        Map<String, Object> result = new HashMap<String, Object>();
        result.put("msg", "登录失败: "+exception.getMessage());
        result.put("status", 500);
        response.setContentType("application/json;charset=UTF-8");
        String s = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(s);
    }
}

在这里插入图片描述
在这里插入图片描述

自定义注销登录配置

登录成功后 ,就处于登录状态 可以访问受保护的资源 。springSecurity默认开启注销登录只需要 访问 “/logout” 即可退出
在这里插入图片描述

在这里插入图片描述
拓展退出登录方式及地址
在这里插入图片描述

注销登录后 返回的页面(传统web开发)

在这里插入图片描述

注销登录后 返回的json(前后端分离)

.logoutUrl(“/logout”) => .logoutSuccessHandler( logoutSuccessHandler )
同登录的Handler、失败的Handler 退出后返回json使用 .LogoutSuccessHandler ,利用多态 传入自定义实现接口的类对象

在这里插入图片描述测试
退出登录 返回的是json
在这里插入图片描述

认证

获取用户认证后信息

在这里插入图片描述

在这里插入图片描述
过程描述

Spring Security 会将登录用户数据保存在 Session 中。但是,为了使用方便,SpringSecurity在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security会将登录成功的用户信息保存到 SecurityContextHolder 中。
​ SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使用 ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,SpringSecurity 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将SecurityContexHolder 中的数据清空。以后每当有请求到来时,Spring Security 就会先从 Session中取出用户登录数据,保存到SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将SecurityContextHolder中的数据清空。

SecurityContextHolder结构

在这里插入图片描述
在这里插入图片描述

SecurityContextHolder:
在这里插入图片描述
在这里插入图片描述
SecurityContextHolder 中 支持的进程 策略
在这里插入图片描述
在这里插入图片描述

  1. MODE THREADLOCAL:这种存放策略是将 SecurityContext 存放在 ThreadLocal中,大家知道 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合 web 应用,因为在默认情况下,一个请求无论经过多少 Filter 到达 Servlet,都是由一个线程来处理的。这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了子线程,在子线程中去获取登录用户数据,就会获取不到。
  2. MODE INHERITABLETHREADLOCAL:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。
  3. MODE GLOBAL:这种存储模式实际上是将数据保存在一个静态变量中,在 JavaWeb开发中,这种模式很少使用到。

在这里插入图片描述

在这里插入图片描述

代码中获取认证信息

测试 在controller中 获取
在这里插入图片描述
开启多线程获取用户信息
在这里插入图片描述
根据源码 但这样为错
在这里插入图片描述

thymeleaf页面中获取认证信息

同样操作SecurityContextHolder,但官网默认提供 ,需要额外引入依赖

  1. 引入 pom文件
 <dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
 </dependency>
  1. 引入约束
<html lang="en" xmlns:th="https://www.thymeleaf.org" 
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
  1. 具体使用
<ul>
  <li sec:authentication="principal.username"></li>
  <li sec:authentication="principal.authorities"></li>
  <li sec:authentication="principal.accountNonExpired"></li>
  <li sec:authentication="principal.accountNonLocked"></li>
  <li sec:authentication="principal.credentialsNonExpired"></li>
</ul>
  1. 配置为页面好controller后,通过浏览器测试 对应结果如下

认证流程

在这里插入图片描述

在这里插入图片描述
AuthenticationManager 的唯一可用实现类 ProviderManager
在这里插入图片描述
AuthenticationManager 、ProviderManager 、AuthenticatioProvider 三者关系
在这里插入图片描述
在源码 ProviderManager类 Authentication authenticate(Authentication authentication) 函数中 出现 :
当本类的所有Provider都认证失败 ,会调用父类

在这里插入图片描述
由于AuthenticationManager 只有一个实现类,所以父类还是它自己 。但构造参数不同,含有的provider发生变化
在这里插入图片描述
高的拓展性,先由局部的ProviderManger 遍历自身所有provider 若都没有支持的在跳转的父类的providerManger 使用它的provider
在这里插入图片描述
小项目通常在兜底的父类中配置认证方式即可,如果其他不成功,肯定为被调用到
在这里插入图片描述
provider通过账户名获取账户信息流程图
在这里插入图片描述

配置全局的AuthenticationProvider

可以通过 AuthenticationManger 的工厂类 给全局的AuthenticationProvider设置调用数据源的封装方法 UserDetailsService()
全局的Provider 即当子providerManger认证失败时 ,调用的父类providerManger时所使用的

方式一:修改原有Builder
在springSecurity的配置类中

 @Autowired
    public void initialize(AuthenticationManagerBuilder builder) throws Exception {
    		对父类promangerProvider中子类 provider的修改,如果没有内容,则完全使用默认builder的配置
    }

springBoot 在工厂中默认创建 AuthenticationManger
直接通过注入获取 builder,和容器中是同一个,是对原有的进行修改
在这里插入图片描述

特点:

  • 由于源代码中设置了数据源,所以会自动寻找 容器中userDetailsService的实现类
  • 会将AuthenticationManager 放入容器,可在其他地方注入

方式二:完全自定义Builder(常用)
完全覆盖掉原有的Builder

   @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        对builder所有的配置,如果没有内容,则Builder的配置为空
    }

特点:

  • 抹去所有工厂中默认配置,需要全部重写,所以要手动指定数据源
  • 默认创建的AuthenticationManager 是工厂内部,相当于new 一个 ,没有放入容器;日后如需要使用AuthenticationManager对象 ,需要在配置类中重写方法 去配置
	@Bean
  @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

在这里插入图片描述
测试
在这里插入图片描述

配置数据源

从上至下 流程:

在这里插入图片描述

  1. 库表
    在这里插入图片描述
    注意 :明文密码前 要加上{noop}
  2. 引入pom文件
<dependency>
       <groupId>org.mybatis.spring.boot</groupId>
       <artifactId>mybatis-spring-boot-starter</artifactId>
       <version>2.2.2</version>
   </dependency>
   
   <dependency>
       <groupId>mysql</groupId>
       <artifactId>mysql-connector-java</artifactId>
   </dependency>
   
   <dependency>
       <groupId>com.alibaba</groupId>
       <artifactId>druid</artifactId>
       <version>1.1.19</version>
   </dependency>
  1. springboot的配置文件
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springsecurity?serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull&autoReconnect=true&useSSL=false&failOverReadOnly=false
    username: root
    password: 1324

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.yan.security.entity
logging:
  level:
    com.yan.security.mapper: debug
  1. 创建实体类
    User要求实现UserDetails接口 ,使得返回规范 。其余User中需要用到的实体类任意

将接口方法改写,返回
在这里插入图片描述
同时补上自身的getter and setter

  1. 创建mapper
   User loadUserByUsername(String name); 查询user表身份
   List<Role> getRolesByUid(Integer id);根据user_role表 和 role 表 查询此用户的权限

1~5 常规的查询操作 ,只是原实体类User要实现 UserDetails接口

  1. 创建自定义的类 实现UserDetailsService 接口,注入容器 使得默认数据源失效
@Component
public class MyUserDetailsService implements UserDetailsService {
    private final UserMapper mapper;
    @Autowired
    public MyUserDetailsService(UserMapper mapper){
        this.mapper=mapper;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = mapper.loadUserByUsername(username);
        if(user==null) throw new UsernameNotFoundException("用户名不存在");
        List<Role> rolesByUid = mapper.getRolesByUid(user.getId());
        user.setRoles(rolesByUid);
        return user;
    }
}

此时 相当于创建了 InMemoryUserDetailsManager 创建了数据源,并加入容器替代默认数据源

  1. 最后一步 让框架使用是自定义的数据源
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    private final MyUserDetailsService service;
    @Autowired
    public WebSecurityConfigurer(MyUserDetailsService service) {
        this.service = service;
    }
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.userDetailsService(service);
    }
}

从此认证表单的用户名,密码 对比数据来自于 数据库

传统web开发认证案例

在这里插入图片描述

  1. 创建项目,搭建环境
    1.1 pom依赖
  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
      <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--thymeleaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
            <version>3.0.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.19</version>
        </dependency>

1.2 springBoot配置

spring:
  thymeleaf:
    cache: false
  security:
    user:
      name: user
      password: 1234
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springsecurity?serverTimezone=UTC
    username: root
    password: 1234
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.yan.security.entity
logging:
  level:
    com.yan.security.mapper: debug

1.3 编写登录页面和主页 thymeleaf页面

1.3.1 index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org"   xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
  欢迎 <span sec:authentication="principal.username"></span>
<a th:href="@{/getUser}">获取用户权限信息</a>
<a th:href="@{/logout}">退出登录</a>
</body>
</html>

1.3.2 login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h2>登录页面</h2>
<form method="post" th:action="@{/toLogin}">
    姓名 <input type="text" name="uname">
    密码 <input type="password" name="passwd">
    <input type="submit" value="提交"/>
</form>
</body>
</html>

1.4 页面放在了资源目录下的template包下 也是thymeleaf视图解析器默认的目录,免得去额外配置页面前置路径
template包是受保护的 ,要想访问到页面 需要视图解析器跳转
方式1:使用Controller逐一跳转
方式2:使用mvc配置

@Configuration
public class MvcConfigure implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/index.html").setViewName("index");
    }
}
  1. 搭建dao环境
    在这里插入图片描述

2.1.1 创建实体类注意User 要实现UserDetails 接口

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        roles.forEach(role->grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())));
        return grantedAuthorities;
    }

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

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

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

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

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

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

    public Integer getId() {
        return id;
    }
    

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }
}

2.1.2 Role 类

public class Role {
    private Integer id;
    private String name;
    private String nameZh;
    public String getName() {
        return name;
    }
}

2.2 创建查询mapper

@Mapper
public interface UserMapper {
   User loadUserByUsername(String name);
    List<Role> getRolesByUid(Integer id);
}

2.3 创建对应的mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yan.security.mapper.UserMapper">
    <!--查询单个-->
    <select id="loadUserByUsername" resultType="User">
        select id,
               username,
               password,
               enabled,
               accountNonExpired,
               accountNonLocked,
               credentialsNonExpired
        from user
        where username = #{username}
    </select>

    <!--查询指定行数据-->
    <select id="getRolesByUid" resultType="Role">
        select r.id,
               r.name,
               r.name_zh nameZh
        from role r,
             user_role ur
        where r.id = ur.rid
          and ur.uid = #{uid}
    </select>
</mapper>
  1. 配置security
    3.1 自定义数据源
@Service
public class MyUserDetailsService implements UserDetailsService {
    private final UserMapper userMapper;
    @Autowired
    public MyUserDetailsService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);
        user.setRoles(userMapper.getRolesByUid(user.getId()));
        return user;
    }
}

3.2 配置拦截规则,跳转规则,设置provider 由于是web开发,响应都采用页面跳转的方式

@Configuration
public class securityConfigure extends WebSecurityConfigurerAdapter {
 
    private final MyUserDetailsService service;
    
    @Autowired
    public securityConfigure(MyUserDetailsService service) {
        this.service = service;
    }
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.userDetailsService(service);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .loginProcessingUrl("/toLogin")
                .defaultSuccessUrl("/index.html",true)
                .failureUrl("/login.html")
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login.html")
                .and()
                .csrf().disable();
    }
}

前后端分离开发认证案例

前后端各是独立的系统 ,后端只提供响应json数据,无法操作前端页面,由前端接收到数据自己判断

认证时 ,与传统web开发不同的是 前端传递的是 json数据 而springSecurity默认采用 request.getParam(“xx”) 方式获取数据

在这里插入图片描述
默认的获取数据方式不满足前后端分离认证要求,需要继承UsernamePasswordAuthenticationFilter 重写attemptAuthentication方法 ,然后使用子类替换父类(要保证子类的过滤器 替代父类的同时,还要占用父类过滤器原来的位置,保持各个过滤器之间顺序不变)

在这里插入图片描述
改写完 UsernamePasswordAuthenticationFilter 的attemptAuthentication()方法后 将改写后的类替换掉原有的
在这里插入图片描述

  1. 搭建dao环境,自定义数据源
@Service
public class MyUserDetailsService implements UserDetailsService {
    private final UserMapper userMapper;
    @Autowired
    public MyUserDetailsService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);
        user.setRoles(userMapper.getRolesByUid(user.getId()));
        return user;
    }
}

和上面传统web开发相同
2. 自定义类继承 UsernamePasswordAuthenticationFilter 重写 attemptAuthentication()

public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
            try {
                Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String username = userInfo.get(getUsernameParameter());
                String password = userInfo.get(getPasswordParameter());
                System.out.println("用户名: " + username + " 密码: " + password);
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return super.attemptAuthentication(request, response);
    }
}

  1. 修改springSecurity的配置
@Configuration
public class SecurityConfigure extends WebSecurityConfigurerAdapter {

    private final MyUserDetailsService service;

    @Autowired
    public SecurityConfigure(MyUserDetailsService service) {
        this.service = service;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.userDetailsService(service);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    @Bean
    public LoginFilter loginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setFilterProcessesUrl("/doLogin");
        loginFilter.setUsernameParameter("uname");
        loginFilter.setPasswordParameter("passwd");
        loginFilter.setAuthenticationManager(authenticationManagerBean());
        //认证成功处理
        loginFilter.setAuthenticationSuccessHandler((req, resp, authentication) -> {
            Map<String, Object> result = new HashMap<String, Object>();
            result.put("msg", "登录成功");
            result.put("用户信息", authentication.getPrincipal());
            resp.setContentType("application/json;charset=UTF-8");
            resp.setStatus(HttpStatus.OK.value());
            String s = new ObjectMapper().writeValueAsString(result);
            resp.getWriter().println(s);
        });
        //认证失败处理
        loginFilter.setAuthenticationFailureHandler((req, resp, ex) -> {
            Map<String, Object> result = new HashMap<String, Object>();
            result.put("msg", "登录失败: " + ex.getMessage());
            resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            resp.setContentType("application/json;charset=UTF-8");
            String s = new ObjectMapper().writeValueAsString(result);
            resp.getWriter().println(s);
        });
        return loginFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint((req, resp, ex) -> {
                    resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    resp.setStatus(HttpStatus.UNAUTHORIZED.value());
                    resp.getWriter().println("请认证之后再去处理!");
                })
                .and()
                .logout()
                .logoutRequestMatcher(new OrRequestMatcher(
                        new AntPathRequestMatcher("/logout", HttpMethod.DELETE.name()),
                        new AntPathRequestMatcher("/logout", HttpMethod.GET.name())
                ))
                .logoutSuccessHandler((req, resp, auth) -> {
                    Map<String, Object> result = new HashMap<String, Object>();
                    result.put("msg", "注销成功");
                    result.put("用户信息", auth.getPrincipal());
                    resp.setContentType("application/json;charset=UTF-8");
                    resp.setStatus(HttpStatus.OK.value());
                    String s = new ObjectMapper().writeValueAsString(result);
                    resp.getWriter().println(s);
                })
                .and()
                .csrf().disable();

        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

测试结果:
在这里插入图片描述
前后端分离,由于登录成功后 服务器会返回写入的cookie 。下次请求携带cookie到服务器查找到session也会实现免登陆

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值