Spring Security使用(二) 异步登录

上一篇文章写了同步的权限校验,今天发一篇异步请求的

目标

现在做项目大部分都是前后端分离的,不是以前那种垂直的,后端的使用都是调用的接口,比如登录,注册等一些操作,今天的目标就是,用SpringSecurity写一个使用接口登录以及登录成功后返回登录的信息,以及当访问没有权限的接口时,返回json,提示它没权限操作

项目

JDK 1.8

IDEA 2020

Springboot 2.4.0

一、创建新项目

1.使用Spring Initializr创建

填写完项目的包名后,在选依赖时,选中这几个就行了

在这里插入图片描述

2.检查下POM有没有缺失

上次因为用Spring Initializr,搞的依赖一直不出来,然后中间一直出错,结果一看pom里面压根没有依赖的坐标,所以养成个习惯。。项目创建好后先看pom.xml

3.配置文件

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.name=defaultDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/liveuser?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456

#Mybatis配置
mybatis.mapper-locations=classpath:/mapper/*.xml

#开启Mybatis下划线命名转驼峰命名
mybatis.configuration.map-underscore-to-camel-case=true

server.port=8080

二、创建数据库

数据库用的还是前面那一篇创建的数据库

/*
 Navicat Premium Data Transfer

 Source Server         : localhost_3306
 Source Server Type    : MySQL
 Source Server Version : 80011
 Source Host           : localhost:3306
 Source Schema         : liveuser

 Target Server Type    : MySQL
 Target Server Version : 80011
 File Encoding         : 65001

 Date: 27/11/2020 18:42:16
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for li_role
-- ----------------------------
DROP TABLE IF EXISTS `li_role`;
CREATE TABLE `li_role`  (
  `id` int(11) NOT NULL,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for li_user
-- ----------------------------
DROP TABLE IF EXISTS `li_user`;
CREATE TABLE `li_user`  (
  `id` int(11) NOT NULL,
  `user` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `pass` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `role` int(255) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

-- 添加权限

INSERT INTO `liveuser`.`li_role`(`id`, `name`) VALUES (1, 'ROLE_ADMIN')
INSERT INTO `liveuser`.`li_role`(`id`, `name`) VALUES (2, 'ROLE_USER')

-- 添加用户
INSERT INTO `li_user`(`id`, `user`, `pass`, `role`) VALUES (1, 'admin', '12345', 1)
INSERT INTO `li_user`(`id`, `user`, `pass`, `role`) VALUES (2, 'user', '12345', 2)

三、设计功能以及SQL语句

还是只有一个登录

SELECT U.ID,U.USER,U.PASS,R.NAME ROLE FROM LI_USER U LEFT JOIN LI_ROLE R ON R.ID = U.ROLE WHERE U.USER = 'username'

四、设计实体类

没有使用Lombok插件,直接创建的getter/setter方法,那个tostring在测试的时候打印到控制台可以看到里面的属性值,而不是它的内存地址了

package com.example.security2.pojo;

public class User {
    Integer id;
    String user;
    
    //在转JSON的时候,忽略Pass这个属性
    @JsonIgnore 
    String pass;
    String role;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUser() {
        return user;
    }

    public void setUser(String user) {
        this.user = user;
    }

    public String getPass() {
        return pass;
    }

    public void setPass(String pass) {
        this.pass = pass;
    }

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }
    
    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", user='" + user + '\'' +
                ", pass='" + pass + '\'' +
                ", role='" + role + '\'' +
                '}';
    }
}


五、写Dao层接口

package com.example.security2.dao;

import com.example.security2.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

@Mapper
@Repository
public interface UserDao {
    User login(@Param("username") String username);
}

UserMapper.xml

上一篇直接用的select注解,今天用xml的方式来写,由于把role直接写实体类里面了,这里就不用写resultMap了,直接使用resultType就行了

<?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.example.security2.dao.UserDao">

    <select id="login" resultType="com.example.security2.pojo.User">
        SELECT U.ID,U.USER,U.PASS,R.NAME ROLE FROM LI_USER U LEFT JOIN LI_ROLE R ON R.ID = U.ROLE WHERE U.USER = #{username}
    </select>


</mapper>

六、业务

ResultVO

package com.example.security2.vo;

import com.fasterxml.jackson.annotation.JsonInclude;

public class ResultVO<E> {
    Integer code;
    @JsonInclude(JsonInclude.Include.NON_NULL)
    String msg;
    @JsonInclude(JsonInclude.Include.NON_NULL)
    E data;

    public  ResultVO(){}
    public ResultVO(Integer code,String msg,E data){
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}

UserService

package com.example.security2.service;

import com.example.security2.dao.UserDao;
import com.example.security2.pojo.User;
import com.example.security2.vo.ResultVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    @Autowired
    UserDao userDao;

    public ResultVO login(String username, String password){
        ResultVO resultVO = new ResultVO();
        try{
            User user = userDao.login(username);
            //迷惑行为,如果密码错了提示用户不存在或密码错误
            if(user == null || !user.getPass().equals(password)){
                resultVO = new ResultVO(0,"用户不存在或密码错误",null);
            }else{
                resultVO = new ResultVO(1,"登录成功",user);
            }

        }catch (Exception ex){
            resultVO = new ResultVO(2,"系统错误",ex.getMessage());
        }finally {
            return resultVO;
        }
    }
}

七、UserController

package com.example.security2.controller;

import com.example.security2.service.UserService;
import com.example.security2.vo.ResultVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    @Autowired
    UserService userService;

    @RequestMapping("/login")
    public ResultVO Login(String username,String password){
        return userService.login(username,password);
    }
}

八、测试登录

访问 localhost:8081/login?username=admin&password=12345

因为我们的账户密码是admin和12345,直接跟着get参数访问就行了,最后获取的是我们要拿到的json

在这里插入图片描述

从图片上可以看到,json已经打印到我们的浏览器上了,这说明我们的接口一切ok

九、写登录后操作的接口

接下来随便写个接口

@RequestMapping("/test")
public String Test(){
    return "test";
}

十、开始配置安全框架

上面是我们不加安全框架的时候的基本操作,接下来我们配置下安全框架

① WebSecurityConfig

package com.example.security2;

import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Configurable
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

    }

    public PasswordEncoder getPasswordEncoder(){
        return  new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return charSequence.toString();
            }

            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return s.equals(charSequence.toString());
            }
        };
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/js/**").permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .loginProcessingUrl("/check")  //异步校验,这个可以随便写路径,到时候使用ajax的时候,让它给这里发请求就行了
                .and()
                .logout().permitAll();
        http.csrf().disable();
    }
}

② 创建CustomUserDetailsService

在创建前我们先修改一下UserService,添加一个newLogin的方法,我们用来验证密码正确放到新建的CustomUserDetailsService里面

public User newLogin(String username){
    return userDao.login(username);
}

下面就是这个新建的service

package com.example.security2.service;

import com.example.security2.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import sun.text.normalizer.ICUBinary;

import java.util.ArrayList;
import java.util.Collection;

@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    UserService userService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.newLogin(username);
        if(user == null){
            throw new UsernameNotFoundException("没有找到这个用户");
        }
        Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        authorities.add(new SimpleGrantedAuthority(user.getRole()));
        return new org.springframework.security.core.userdetails.User(user.getUser(), user.getPass(), authorities); 		
    }
}

这个返回的user(org.springframework.security.core.userdetails.User)只有用户名,密码还有它的权限,如果我们User类中有其他信息的字段,怎么带过去呢??

我们先新建一个NewUserDetail 让他继承UserDetail类

package com.example.security2;

import com.example.security2.vo.ResultVO;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

//让他继承 org.springframework.security.core.userdetails.User
public class NewUserDetails extends User {
    com.example.security2.pojo.User userInfo;  //创建我们的user实体类属性

    public NewUserDetails(com.example.security2.pojo.User user, Collection<? extends GrantedAuthority> authorities) 	{
        super(user.getUser(), user.getPass(), authorities); //这里让他去执行它父类的构造函数
        this.userInfo = user; //将传过来的用户实体类赋值给userinfo属性
    }

    public NewUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
    }

    //新建一个获取ResultVO的类,在这个方法里面创建我们的VO,并把数据添加进去
    public ResultVO getResultVO(){
        ResultVO resultVO = new ResultVO();
        resultVO.setCode(1);
        resultVO.setMsg("登录成功");
        resultVO.setData(this.userInfo);
        return resultVO;
    }


}

接下来修改一下我们的CustomUserDetailsService

package com.example.security2.service;

import com.example.security2.NewUserDetails;
import com.example.security2.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import sun.text.normalizer.ICUBinary;

import java.util.ArrayList;
import java.util.Collection;

@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    UserService userService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.newLogin(username);
        if(user == null){
            throw new UsernameNotFoundException("没有找到这个用户");
        }
        Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        authorities.add(new SimpleGrantedAuthority(user.getRole()));
        return new NewUserDetails(user,authorities); //将这里的返回创建成我们刚刚新建的一个user
    }
}

③ 创建登录成功后的拦截器

为了接收到登录成功的信息,我们先要实现下 AuthenticationSuccessHandler这个接口

package com.example.security2;

import com.fasterxml.jackson.databind.ObjectMapper;
import jdk.nashorn.internal.parser.JSONParser;
import org.apache.ibatis.reflection.wrapper.ObjectWrapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

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

@Component
public class SuccessHandlerImpl implements AuthenticationSuccessHandler {
    ObjectMapper objectMapper;
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {

    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("登录成功");
        response.setCharacterEncoding("UTF-8"); //设置输出信息的编码格式
        NewUserDetails user= (NewUserDetails) authentication.getPrincipal(); //获取登录用户的主体
        System.out.println(user.getResultVO()); //这里打印测试下是不是我们要的信息
        PrintWriter wirter = response.getWriter();
        
        //下面的objectMapper是jackson中转json的一个对象
        if(objectMapper == null){
            objectMapper  = new ObjectMapper();
        }
        wirter.println(objectMapper.writeValueAsString(user.getResultVO()));  //然他给屏幕上打印我们的json
    }
}

④ 创建登录失败后的拦截器

登录失败的时候我们应该也是要返回一段json,创建FailHandleImpl类,让他实现AuthenticationFailureHandler接口

package com.example.security2;

import com.example.security2.vo.ResultVO;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

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

@Component
public class FailHandleImpl implements AuthenticationFailureHandler {
    ObjectMapper objectMapper;
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.out.println("登录失败");
        response.setCharacterEncoding("UTF-8"); //设置输出信息的编码格式
        PrintWriter wirter = response.getWriter();
        if(objectMapper == null){
            objectMapper  = new ObjectMapper();
        }
        ResultVO resultVO = new ResultVO();
        resultVO.setCode(1);
        resultVO.setMsg("登录失败,用户名或密码错误");
        wirter.println(objectMapper.writeValueAsString(resultVO));

    }
}

⑤ 创建WebSecurityConfig类

安全框架的配置类

package com.example.security2;

import com.example.security2.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Configurable
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
   @Autowired
   CustomUserDetailsService customUserDetailsService;
   @Autowired
   SuccessHandlerImpl successHandler;
   @Autowired
   FailHandleImpl failHandle;
   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(customUserDetailsService).passwordEncoder(getPasswordEncoder());
   }

   public PasswordEncoder getPasswordEncoder(){
       return  new PasswordEncoder() {
           @Override
           public String encode(CharSequence charSequence) {
               return charSequence.toString();
           }

           @Override
           public boolean matches(CharSequence charSequence, String s) {
               return s.equals(charSequence.toString());
           }
       };
   }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests()
               .antMatchers("/js/**").permitAll() //这里用来释放我们的js文件夹内的文件,也就是让jq或者其他js不用登录通行
               .anyRequest().authenticated()
               .and().formLogin()
               .loginProcessingUrl("/check")  //异步校验
               .successHandler(successHandler) //登录成功的拦截器,使用我们刚刚创建的SuccessHandlerImpl
               .failureHandler(failHandle) //登录失败拦截器,FailHandleImpl
               .and()
               .logout().permitAll();
       http.csrf().disable();
   }
}

这个时候,我们的登录接口已经完成了,我们测试一下
看下postman测试打印的内容,成功获取到我们登录的信息

在这里插入图片描述

这次故意输错用户名

在这里插入图片描述

十一、添加权限校验

① 添加权限接口的权限

我们在UserController类中Test方法上加上权限

@RequestMapping("/test")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String Test(){
    return "test";
}

② 测试

我们先请求登录接口登录admin用户,然后在请求test看看是不是输出的test这个字符串

在这里插入图片描述

接着postman请求登录接口,登录我们的user用户,然后在用postman请求/test,看下输出什么

在这里插入图片描述

403我们有没有办法让他返回JSON,提示权限不足? 往下看

我们创建一个AccessHandleImpl类,让他实现 AccessDeniedHandler接口

package com.example.security2;

import com.example.security2.vo.ResultVO;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

@Component
public class AccessHandleImpl implements AccessDeniedHandler {
    ObjectMapper objectMapper;
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        System.out.println("登录失败");
        response.setCharacterEncoding("UTF-8"); //设置输出信息的编码格式
        PrintWriter wirter = response.getWriter();
        if(objectMapper == null){
            objectMapper  = new ObjectMapper();
        }
        ResultVO resultVO = new ResultVO();
        resultVO.setCode(2);
        resultVO.setMsg("权限不足");
        wirter.println(objectMapper.writeValueAsString(resultVO));

    }
}

修改WebSecurityConfig类

http.authorizeRequests()
        .antMatchers("/js/**").permitAll()
        .anyRequest().authenticated()
        .and().formLogin()
        .loginProcessingUrl("/check")  //异步校验
        .successHandler(successHandler)
        .failureHandler(failHandle)
        .permitAll()
        .and()
        .exceptionHandling().accessDeniedHandler(accessHandle) //这里加上我们没有权限时的拦截器
        .and()
        .logout().permitAll();
http.csrf().disable();

接下来在测试一下

在这里插入图片描述

又遇见个问题,如果我们没有登录,然后去直接访问test,则会给我们返回 默认的登录页面,在postman中会看到这个登录页面的html代码,这个怎么解决??

在这里插入图片描述

我们在UserController类中加一个方法

@RequestMapping("/logintips")
public ResultVO noLogin(){
    return new ResultVO(-1,"未登录",null);  //为什么上面没有用这个构造方法直接用的setter,因为。。在前面用构造方法的时候,msg这里一直是null,然后没有找到原因,写到这里的时候发现,,我把msg写成msgm了。。
}

然后在安全框架配置类中添加一个默认的登录页到这个logintips上就好了

.loginPage("/logintips")

在这里插入图片描述

然后在来测试一下看看效果

在这里插入图片描述

欢迎访问:http://www.fanxing.live

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页
实付 9.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值