Spring Security前后端分离认证及记住我踩坑

Spring Security前后端分离认证

前言:Spring Security是spring提供的一个安全框架,提供了登录认证、密码保护、自动登录等。这是我目前学习到的功能,当然它的强大之处远不止这些了。Spring Security处理登录功能时使用的
form表单,底层获取参数使用的request.getParamter的形式获取的。但是前后端分离模式使用的是异步请求,所以在前后端分离模式下会出现很多问题。以下记录了我出现过的问题。

这里后端使用的是springboot+spring security,使用插件fastjson以及lombok
前端使用Vue+axios+ElementUi(主要为了操作简单)

使用Spring Security主要操作

  • 编写Security配置类,继承WebSecurityConfigurerAdapter类,重写两个重要的方法configure(HttpSecurity http)和configure(AuthenticationManagerBuilder auth);参数为http的方法主要配置拦截请求后的操作,比如登录页面,登陆了成功操作,失败操作,或者跨域请求登录问题。参数为auth主要是配置登录认证时使用自定义认证方法。这里需要注意一下几点:

  • 必须配置PasswordEncoder @Bean对象, Security的底层是强制对密码进行编码的,如果没有配置,会报错PasswordEncoder为null。

  • Security需要开启跨域访问,代码在参数为http的方法编写

  • Security内部有自带的login页面,当未登录时会默认跳转改页面,如果是前后端分离模式,则会出现302的错误。需要将跳转重新设置,前后端分离情况下不需要返回页面,只需要返回未登录信息,所以应该写一个mapping方法,返回未登录信息,如下:

@RestController
public class LoginController {
    
    
    @GetMapping("/unLogin")
    public String unLogin(){
    
    
        return "未登录";
    }
}

并在配置方法中配置登录页跳转即可: http.loginPage("/unLogin")

  • 默认security处理登录失败,登录成功,退出登录跳转之类的使用的是页面表示,对于前后端模式下只需要返回状态即可,这里可以使用XXXHandler进行处理,使用response返回信息即可。
  • 其他需要注意的细节在代码中标注。
    具体代码如下:
package com.example.securitydemo.config;

import com.alibaba.fastjson.JSON;
import com.example.securitydemo.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    
	//自定义Sevice实现UserDetails
    @Autowired
    private MyUserDetailsService myUserDetailsService;
    //前提配置文件中中文件置配置数据源
    @Autowired
    private DataSource dataSource;
    /**
     *  实现自动登录功能,配置数据源
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
    
    
        //使用JDBC
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //创建数据库,只能执行一次,第二次之后需要注掉,否则报错数据库已存在
//        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

    /**
     *
     * 如果需要实现自己的编码格式,可以实现PasswordEncoder接口,重写encode()方法实现编码
     * 设置编码格式,必须设置,否则报错
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return new BCryptPasswordEncoder();
    }

    /**
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        //注入自己的userDetailsService
        auth.userDetailsService(myUserDetailsService)
                //设置密码编码格式
                .passwordEncoder(passwordEncoder());
    }
    /**
     * 拦截请求后的权限设置
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.authorizeRequests()
                .antMatchers("/test1").authenticated()//设置必须持有凭证才可以访问路径
                .antMatchers("/test2").permitAll()//设置无需凭证访问路径
                .and().formLogin() //使用自带的登录
                //登录页表示未登录的时候跳转的路径
                .loginPage("/unLogin")
                .loginProcessingUrl("/login")//设置登录请求路径
                //登录失败,返回json
                .failureHandler((request, response, ex) -> {
    
    
                    response.setContentType("application/json;charset=utf-8");
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    PrintWriter out = response.getWriter();
                    Map<String, Object> map = new HashMap();
                    map.put("code", 401);
                    if (ex instanceof UsernameNotFoundException || ex instanceof BadCredentialsException) {
    
    
                        map.put("message", "用户名或密码错误");
                    } else if (ex instanceof DisabledException) {
    
    
                        map.put("message", "账户被禁用");
                    } else {
    
    
                        map.put("message", "登录失败!");
                    }
                    out.println(JSON.toJSONString(map));
                    out.flush();
                    out.close();
                })
                //登录成功,返回json
                .successHandler((request, response, authentication) -> {
    
    
                    Map<String, Object> map = new HashMap();
                    map.put("code", 200);
                    map.put("message", "登录成功");
                    map.put("data", authentication);
                    response.setContentType("application/json;charset=utf-8");
                    PrintWriter out = response.getWriter();
                    out.println(JSON.toJSONString(map));
                    out.flush();
                    out.close();
                })
                .and()
                .exceptionHandling()
                //没有权限,返回json
                .accessDeniedHandler((request, response, ex) -> {
    
    
                    response.setContentType("application/json;charset=utf-8");
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    PrintWriter out = response.getWriter();
                    Map<String, Object> map = new HashMap();
                    map.put("code", 403);
                    map.put("message", "权限不足");
                    out.println(JSON.toJSONString(map));
                    out.flush();
                    out.close();
                })
                //自动登录(记住我功能)
                .and()
                .rememberMe().tokenRepository(persistentTokenRepository())
                .rememberMeParameter("rememberMe")//参数名称
                .userDetailsService(myUserDetailsService)//使用自定义service操作数据库
                .tokenValiditySeconds(60)//已秒为单位
                .and()
                .logout()
                //退出登录url
                .logoutUrl("/logout")
                //退出成功,返回json
                .logoutSuccessHandler((request, response, authentication) -> {
    
    
                    Map<String, Object> map = new HashMap();
                    map.put("code", 200);
                    map.put("message", "退出成功");
                    map.put("data", authentication);
                    response.setContentType("application/json;charset=utf-8");
                    PrintWriter out = response.getWriter();
                    out.println(JSON.toJSONString(map));
                    out.flush();
                    out.close();
                })
                .permitAll()
                //开启跨域访问
                .and().cors()
                //开启模拟请求,比如API POST测试工具的测试,不开启时,API POST为报403错误
                .and().csrf().disable();
    }

}

  1. 上面有提过自定义认证方法,security底层认证过程是通过一个UserDetailsService的实现类,实现了loadUserByUsername(String username)方法,进行验证的,所以这里创建了MyUserDetailsService进行重写这个方法实现自定义登录验证方法。在这里可以在使用JDBC的方式访问数据库,查询username账户,账户不存在表示数据库没有数据,账户存在并对密码进行加密,然后返回一个UserDetails对象,然后可以security底层去判断是否登录成功。需要构建的代码如下:
package com.example.securitydemo.service;

import com.example.securitydemo.entity.User;
import com.example.securitydemo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class MyUserDetailsService implements UserDetailsService {
    
    
    @Autowired
    private UserMapper userMapper;

    /**
     * 重写loadUserByUsername方法,实现登录验证
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
        User user = userMapper.selectByUserName(username);
        //如果用户名不存在
        if (user == null) {
    
    
            throw new UsernameNotFoundException("用户名不存在");
        }
        user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));

        return user;
    }
}

package com.example.securitydemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {
    
    
    private String username;
    private String password;

    /**
     * 访问权限的角色,这里可以定义用户的访问权限
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        //访问角色,这里暂时没有用。据网上说这里设置为null会显示没有访问权限登录不进去。
        // 但是我没有出现过这个问题,这里先简单的设置一下,不过没有什么用处 
        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        return authorities;
    }

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

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

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

    @Override
    public boolean isEnabled() {
    
    
        return true;
    }
}
  1. 接下来就是前端页面设置了,前端界面很简单,但是需要注意的是,登录账户名必须是username,密码必须是password,记住我的复选框名称必须是rememberMe,这里security的底层这样定义名字的, 可以在security的配置文件中修改名称。在我这里的配置中必须使用这个名字,否则报错。页面代码如下:
<!-- 这里最主要的是el-input的代码,其他的是是elementUi的修饰。这里的v-model绑定的是输入框的名称 -->
<div style="width: 300px;height: 100px;">
      <el-form ref="form" :model="form" label-width="80px">
        <el-form-item label="账号">
          <el-input v-model="form.username"></el-input>
        </el-form-item>
        <el-form-item label="密码">
          <el-input v-model="form.password"></el-input>
        </el-form-item>
        <el-form-item>
          <el-checkbox v-model="form.rememberMe">记住我</el-checkbox>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="onSubmit">登录</el-button>
          <el-button>取消</el-button>
        </el-form-item>
      </el-form>
    </div>
  1. 提交的代码时,axios使用的是JSON对象的格式,并不是form表单格式,网上很多都是
    重写security的UsernamePasswordAuthenticationFilter这个过滤器,使其支持对象接收,但是在处理机复选框的时候会出现问题。这里解决的办法是:**既然security处理的是表单,那么前端传递的时候就传递成表单格式就可以了。这里使用Vue自带的qs进行表单格式处理。这样在不修改security的逻辑下,解决了参数问题,避免了重写自定义功能后出现其他异常的连锁反应。**前端完整代码如下:
<template>
  <div id="app">
    <div style="width: 300px;height: 100px;">
      <el-form ref="form" :model="form" label-width="80px">
        <el-form-item label="账号">
          <el-input v-model="form.username"></el-input>
        </el-form-item>
        <el-form-item label="密码">
          <el-input v-model="form.password"></el-input>
        </el-form-item>
        <el-form-item>
          <el-checkbox v-model="form.rememberMe">记住我</el-checkbox>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="onSubmit">登录</el-button>
          <el-button>取消</el-button>
        </el-form-item>
      </el-form>
    </div>

  </div>
</template>

<script>
  export default {
     
     
    data() {
     
     
      return {
     
     
        form: {
     
     }
      }
    },
    methods: {
     
     
      onSubmit() {
     
     
        let qs = require('qs');
        //使用qs将对象转换为form表单
        this.$axios.post("/login",qs.stringify(this.form))
          .then((response) => {
     
     
            console.log(response)
          }).catch((err) => {
     
     
            console.log(err)
          })
      }
    }
  }
</script>

<style>
  #app {
     
     }
</style>

以上就是我遇到的问题。前端的额外处理这里就不介绍了,可以在axios的拦截器功能中拦截请求,如果登录失败的*跳转登录页即可。记住我的功能可以访问后端获取用户信息,这样当用户不能访问信息时,表示账户登录信息失效。非常完美的框架

猜你喜欢

转载自blog.csdn.net/qq_30385099/article/details/113081859