spring security环境搭建及表单认证

教学目标

  • 上次作业问题

  • SpringSecurity

    • SpringSecurity简介

    • 项目环境搭建

    • SpringSecurity认证

1.作业问题

1-1 axios文件上传并提交表单数据 如何实现?

方法1:

html页面

<template id="userAddTempl">
   <div>
       <h3>用户添加</h3>
       <hr>
       <p>编码: <input type="text" v-model="user.usercode"></p>
       <p>密码: <input type="text" v-model="user.userpassword"></p>
       <p>姓名: <input type="text" v-model="user.userName"></p>
       <p>性别:
           <select v-model="user.gender">
               <option value="">请选择</option>
               <option value="1"></option>
               <option value="2"></option>
           </select>
       </p>
       <p>生日: <input type="date" v-model="user.birthday"></p>
       <p>电话: <input type="text" v-model="user.phone"></p>
       <p>地址: <input type="text" v-model="user.address"></p>
       <p>角色: <select v-model="user.roleid">
                   <option value="">请选择</option>
                   <option :value="role.id" v-for="role in roleList">{{role.rolename}}</option>
               </select>
       </p>
       <p>头像: <input type="file" id="avatarImg" style="width:200px;"></p>
       <p>
           <input type="button" value="确定" @click="doAddUser()">
           <input type="button" value="返回列表" @click="goBack()">
       </p>
   </div>
</template>

methods:{
       doAddUser(){
           let file = document.getElementById("avatarImg").files[0];
           console.log(file);
           let formData = new FormData();
           //这里将表单数据封装成json保存至formData中
           formData.append("formUser",new Blob([JSON.stringify(this.user)],{type: "application/json"}));

           formData.append("uploadFile",file);
           const config = {
               headers: { "Content-Type": "multipart/form-data;boundary="+new Date().getTime() }
            };

           axios.post("/doAddUser",formData,config).then(res=>{
               if(res.status==200){
                   alert("添加成功!");
                   this.$router.push("/userList");
               }
           });
       }
   },

Controller:

 /*
    * 1.@RequestPart这个注解用在multipart/form-data表单提交请求的方法上。
    * 2.支持的请求方法的方式MultipartFile,属于Spring的MultipartResolver类。这个请求是通过http协议传输的。
    * 3.@RequestParam也同样支持multipart/form-data请求。
    * 4.他们最大的不同是,当请求方法的请求参数类型不再是String类型的时候。
    * 5.@RequestParam适用于name-valueString类型的请求域,@RequestPart适用于复杂的请求域(像JSON,XML)。
   */
   @RequestMapping("/doAddUser")
   public ResponseEntity<String> doAddUser(@RequestPart("formUser") User user, MultipartFile uploadFile) {
       //idpicpath
       System.out.println(user);

       try {
           InputStream inputStream = uploadFile.getInputStream();

           String fname = uploadFile.getOriginalFilename();
           String suffix = fname.substring(fname.lastIndexOf("."));
           String fileName = UUID.randomUUID().toString().replaceAll("-", "") + suffix;

           String uploadPath = ResourceUtils.getURL("classpath:").getPath() + "static/upload/";
           File destDir = new File(uploadPath);
           if (!destDir.exists()) {
               destDir.mkdirs();
           }
           //创建目标文件
           File destFile = new File(uploadPath, fileName);

           try {
               //使用工具类方法将文件拷贝
               FileCopyUtils.copy(uploadFile.getInputStream(), new FileOutputStream(destFile));
           } catch (IOException exception) {
               return new ResponseEntity<String>("上传文件失败", HttpStatus.BAD_REQUEST);
           }
       } catch (IOException e) {
           e.printStackTrace();
       }
       return new ResponseEntity<String>("添加成功", HttpStatus.OK);
   }
}

方法2:

 methods:{
       doAddUser(){
           let file = document.getElementById("avatarImg").files[0];
           console.log(file);
           let formData = new FormData();
        formData.append("usercode",this.user.userCode);
           formData.append("userName",this.user.userName);
           formData.append("gender",this.user.gender);
           formData.append("phone",this.user.phone);
           formData.append("birthday",this.user.birthday);
           //....
         
           formData.append("uploadPic",file);
           const config = {
               headers: { "Content-Type": "multipart/form-data;boundary="+new Date().getTime() }
            };

           axios.post("/doAddUser",formData,config).then(res=>{
               if(res.status==200){
                   alert("添加成功!");
                   this.$router.push("/userList");
               }
           });
       }
   },

Controller:

@RequestMapping("/doAddUser")
public ResponseEntity<String> doAddUser(User user,@RequestParam("uploadPic") MultipartFile uploadFile) {
   //idpicpath
   System.out.println(user);

   try {
       InputStream inputStream = uploadFile.getInputStream();

       String fname = uploadFile.getOriginalFilename();
       String suffix = fname.substring(fname.lastIndexOf("."));
       String fileName = UUID.randomUUID().toString().replaceAll("-", "") + suffix;

       String uploadPath = ResourceUtils.getURL("classpath:").getPath() + "static/upload/";
       File destDir = new File(uploadPath);
       if (!destDir.exists()) {
           destDir.mkdirs();
       }
       //创建目标文件
       File destFile = new File(uploadPath, fileName);

       try {
          //或者:  uploadFile.transferTo(destFile)
           //使用工具类方法将文件拷贝 commons-fileupload.jar依赖
           FileCopyUtils.copy(uploadFile.getInputStream(), new FileOutputStream(destFile));
       } catch (IOException exception) {
           return new ResponseEntity<String>("上传文件失败", HttpStatus.BAD_REQUEST);
       }
   } catch (IOException e) {
       e.printStackTrace();
   }
   return new ResponseEntity<String>("添加成功", HttpStatus.OK);
}

1-2 使用mybatisplus执行多表连接查询

在实体类上添加了关联的字段

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("smbms_user")
public class User implements Serializable {

   private static final long serialVersionUID = 1L;

   /**
    * 主键ID
    */
   @TableId(value = "id", type = IdType.AUTO)
   private Integer id;

   /**
    * 用户编码
    */
   @TableField("userCode")
   private String userCode;

   /**
    * 用户名称
    */
   private String userName;

   /**
    * 用户密码
    */
   @TableField("userPassword")
   private String userPassword;

   /**
    * 性别(1:女、 2:男)
    */
   private Integer gender;

   /**
    * 出生日期
    */
   private Date birthday;

   /**
    * 手机
    */
   private String phone;

   /**
    * 地址
    */
   private String address;

   /**
    * 用户角色(取自角色表-角色id)
    */
   private Integer roleid;

   /**
    * 创建者(userId)
    */
   @TableField("createdBy")
   private Integer createdBy;

   /**
    * 创建时间
    */
   @TableField("creationDate")
   private Date creationDate;

   /**
    * 更新者(userId)
    */
   @TableField("modifyBy")
   private Integer modifyBy;

   /**
    * 更新时间
    */
   @TableField("modifyDate")
   private Date modifyDate;

   @TableField("IDPICPATH")
   private String idpicpath;


   @TableField(exist = false)  //表示mybatisplus自带的CRUD  不去使用此字段
   private Role role;

}

mybatisplus内置的crud都不能执行,需要在上面添加注解

 @TableField(exist = false)

2.SpringSecurity

2-1 简介

Spring Security 安全框架

|-两大核心功能:

认证 - authenticate 【动词】 、authentication [身份验证; 认证]

|- 身份进行认证的认证方式 -- 登录系统时,登录操作

鉴权 -  authorization 【批准; 授权】

  authority 权力

鉴权 是建立在认通过以后的操作

image-20220704113250243

SpringSecurity特点:

a. 身份验证和访问控制框架
b. Spring Security是一个专注于为Java Spring应用程序提供 身份验证和 授权的框架
c. Spring Security的真正强大之处在于它可以轻松扩展以满足自定义要求

SpringSecurity与shiro

Shiro比Spring security更容易使用,实现和最重要的理解
Spring Security更加知名的唯一原因是因为品牌名称
Spring-security 对Spring结合较好,如果项目用的SpringMVC,使用起来很方便。

2-2 项目环境搭建

使用Springboot脚手架-SpringSecruity Demo

image-20220704114318187

  • login.html为登录页面,都可以访问

  • 在登录成功之后,进入home.html主页

  • 主页中有四个超链接,分别对应我们将要实现的四个功能用户管理、角色管理...

  • 但是四个页面需要有不同的权限,主要看红色的字体

启动时,由于添加springSecurity,会出一行:

Using generated security password: 04765d11-339d-492f-b55f-eb75e1135a5a
生成的默认登录的密码
默认的用户名是user

2-3 SpringSecurity认证

(1) HttpBasic模式登录认证

SpringSecurity 自带一种基础认证模式

HttpBasic登录验证模式是Spring Security实现登录验证最简单的一种方式,也可以说是最简陋的一种方式。它的目的并不是保障登录验证的绝对安全,而是提供一种“防君子不防小人”的登录验证

我们现在使用的是spring boot2.0版本(依赖Security 5.X版本),HttpBasic不再是默认的验证模式,在spring security 5.x默认的验证模式已经是表单模式。所以我们要使用Basic模式,需要在配置类中去配置

Basic模式登录认证不是表单,而是一个简陋的对话框

实现方式:创建SpringSecurity配置类

WebSecurityConfigurerAdapter是适配器类, 在配置的时候需要我们自己写配置类去继承他,然后编写自己所特殊需要的配置

/**
* Spring Securtiy配置类
*/
@Configuration  //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       super.configure(auth);
   }

   @Override
   public void configure(WebSecurity web) throws Exception {
       super.configure(web);
   }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       //开启httpBasci模式登录认证
       http.httpBasic()
               //每个模块配置使用and结尾
               .and()
               //配置路径拦截,表明路径访问所对应的权限,角色,认证信息
               .authorizeRequests()
               .anyRequest()
               //所有请求都需要登录认证才能访问
               .authenticated();
   }
}

(2) 默认formLogin表单模式

在spring security 5.x默认的验证模式已经是表单模式,接下来要讲的formLogin模式,注释掉@Configuration,方便测试

//@Configuration  //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
     ...
}

可以通过applicaton.yml配置用户名与密码

spring:
   security:
           user:
               name: tom
               password: tom

(3) 自定义formLogin表单模式

/**
* Spring Securtiy配置类
*/
@Configuration  //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       super.configure(auth);
   }

   @Override
   public void configure(WebSecurity web) throws Exception {
       super.configure(web);
   }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       //开启自定义表单模式登录认证
       //需要放行的url在这里配置,必须要放行/login和/login.html,不然会报错
       http.authorizeRequests().antMatchers("/login", "/login.html")
               .permitAll().anyRequest().authenticated()
               .and().
               // 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
                     formLogin().loginPage("/login.html").loginProcessingUrl("/login")
               //登录表单form中密码输入框input的name名,不修改的话默认是password
               .usernameParameter("username").passwordParameter("password")
               //登录认证成功后默认转跳的路径
               .defaultSuccessUrl("/home");
       //关闭CSRF跨域攻击防御
       http.csrf().disable();
   }
}

注:由于我们现在是使用静态login.html登录,此页面没有读取服务端生成的token值,在这里我们暂时关闭csrf跨域攻击的防御机制

(4) 认证的流程底层分析

自定义formLogin表单认证流程分析:

javax.servlet.Filter接口

当第一次发起请求时,首先调用DelegatingFilterProxy过滤器的doFilter()方法

DelegatingFilterProxy就是一个对于servlet filter的代理,用这个类的优势主要是通过Spring容器来管理servlet filter的生命周期,本质上来说DelegatingFilterProxy就是一个Filter

启动一个FilterProxyChain 过滤器代理的链 【根据请求加载一组过滤器】

DelegatingFilterProxy:

  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
103行设置断点:  Filter delegateToUse = this.delegate;
       
        //...
 }

FilterChainProxy过滤器链接代理类

在FilterChainProxy的dofilter方法中,会去调用来获取该过滤链里面的过滤器的个数,然后采用设计模式中的责任链模式依次顺序调用对应的过滤器的dofilter方法,一共是11个(WebAsyncManagerIntegrationFilter、SecurityContextPersistenceFilter、HeaderWriterFilter 、LogoutFilter 、UsernamePasswordAuthenticationFilter、RequestCacheAwareFilter、SecurityContextHolderAwareRequestFilter、AnonymousAuthenticationFilter、SessionManagementFilter、ExceptionTranslationFilter、FilterSecurityInterceptor)

public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
           if (this.currentPosition == this.size) {
               if (FilterChainProxy.logger.isDebugEnabled()) {
                   FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " reached end of additional filter chain; proceeding with original chain");
               }

               this.firewalledRequest.reset();
               this.originalChain.doFilter(request, response);
           } else {
               ++this.currentPosition;
               Filter nextFilter = (Filter)this.additionalFilters.get(this.currentPosition - 1);
               if (FilterChainProxy.logger.isDebugEnabled()) {
                   FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " at position " + this.currentPosition + " of " + this.size + " in additional filter chain; firing Filter: '" + nextFilter.getClass().getSimpleName() + "'");
               }

// 170行 断点   nextFilter.doFilter(request, response, this);
           }
}

image-20220704154921614

(5) PasswordEncoder密码加密

Spring Security封装了如bcrypt, PBKDF2, scrypt, Argon2等主流适应性单向加密方法( adaptive one-way functions),用以进行密码存储和校验

public interface PasswordEncoder {
   String encode(CharSequence var1); //将原始密码加密 '123'

   boolean matches(CharSequence var1, String var2); //密码匹配参数1为原始密码

   default boolean upgradeEncoding(String encodedPassword) {
       return false;     //设置是否需要升级
   }
}

步骤1: 首先在启动类上创建 Bean注解方法

@SpringBootApplication
public class SpringbootSecurityDemoApplication {

   public static void main(String[] args) {
       SpringApplication.run(SpringbootSecurityDemoApplication.class, args);
   }

   //创建一个PasswordEncoder加密器存入容器中
   @Bean
   public PasswordEncoder getPasswordEncoder(){
       return new BCryptPasswordEncoder();
   }
}

步骤2: 在测试类中测试PasswordEncoder方法

@SpringBootTest
class SpringbootSecurityDemoApplicationTests {
   @Autowired
   private PasswordEncoder passwordEncoder;

   @Test
   void contextLoads() {
       String pwd1 = passwordEncoder.encode("123");
       String pwd2 = passwordEncoder.encode("123");
       System.out.println(pwd1); //$2a$10$21Ae7HsPihdZNu.YWhqeHu4dJ5/45l5mpQxN8P3HWzPNlDeh84cm2
       System.out.println(pwd2); //$2a$10$000Misx3wpASVQkzz.iyK.q9L9qx7Thl7mey/UzoLEAvVJgenvGc.
       System.out.println(passwordEncoder.matches("123",pwd1));
       System.out.println(passwordEncoder.matches("123",pwd2));
       //$2a$10$s65QPon1AZTjZhQjlL.jF.8JBeIjdPLoL.UfZkDA5Uzv94yQhq.RG
       //$2a$10$/Rs7Zr41nC6UWgP/Ij0tnOnrBwMLjwRDYrGJrNMzMMlWQB5XgmBJe
   }
}

步骤3: 在配置类中使用加密的方式在内存中配置用户名和密码,修改类WebSecurityConfig

/**
* Spring Securtiy配置类
*/
@Configuration  //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
   @Autowired
   private PasswordEncoder passwordEncoder;

   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.inMemoryAuthentication().withUser("tom")
               .password(passwordEncoder.encode("123")).roles();
       auth.inMemoryAuthentication().withUser("admin")
               .password(passwordEncoder.encode("admin")).roles();
   }
   
   ...
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值