Spring Security使用(二) 非同步登入

Fanx繁星發表於2020-11-29

上一篇文章寫了同步的許可權校驗,今天發一篇非同步請求的

目標

現在做專案大部分都是前後端分離的,不是以前那種垂直的,後端的使用都是呼叫的介面,比如登入,註冊等一些操作,今天的目標就是,用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

相關文章