SpringSecurity04 利用JPA和SpringSecurity实现前后端分离的认证和授权

 

 

1 环境搭建

  1.1 环境说明

    JDK:1.8

    MAVEN:3.5

    SpringBoot:2.0.4

    SpringSecurity:5.0.7

    IDEA:2017.02旗舰版

  1.2 环境搭建

    创建一个SpringBoot项目,引入相关依赖:WEB、JPA、MYSQL、SpringSecurity、lombok、devtools

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.xunyji</groupId>
    <artifactId>springsecurity03</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>springsecurity03</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>
pom.xml

  1.3 MySQL连接信息配置

    在yml配置文件中配置MySQL相关的配置,参考文档

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1/testdemo?characterEncoding=utf-8&useSSL=false
    username: root
    password: 182838


  jpa:
    properties:
      hibernate:
        format_sql: true
        show_sql: true
application.yml

  1.4 创建一个Restful接口

    该接口主要用于测试用的

package com.xunyji.springsecurity03.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 王杨帅
 * @create 2018-09-08 21:51
 * @desc
 **/
@RestController
@RequestMapping(value = "/test")
@Slf4j
public class TestController {

    @GetMapping(value = "/home")
    public String home() {
        String info = "寻渝记主页面";
        log.info(info);
        return info;
    }

}
TestController.java

  1.5 启动应用

    技巧01:SpringBoot集成了SpringSecurity后,默认会对除了 /login 的所有请求进行拦截认证,所以我们启动应用后访问 /test/home 时会自动重定向到 /login 去进行登录录入

    技巧02:SpringSecurity默认提供了一个用户,用户名是 user, 用户密码在启动应用时会打印输出到控制台

    技巧03:SpringSecurity默认在登录成功后会自动跳转到之前访问的请求【PS: 这种方式在前后端分离的项目中并不适用,所以需要进行登录成功和失败来接,让登录成功和失败后返回JSON信息给前端,具体怎么跳转有前端进行控制】

 

2 基于内存的认证和授权

  待更新......2018年9月8日22:01:11

  参考文档

 

3 基于JPA实现SpringSecurity的认证和授权

  3.1 创建数据库表

    用户表:用于存放用户的相关信息

    角色表:用于存放角色信息【PS: 角色表插入数据时必须全部大写,而且要以 ROLE_ 开头】

    用户角色表:用户存放用户和角色的关联信息【PS: 用户和角色是多对多的关系】

/*
Navicat MySQL Data Transfer

Source Server         : mysql5.4
Source Server Version : 50540
Source Host           : localhost:3306
Source Database       : testdemo

Target Server Type    : MYSQL
Target Server Version : 50540
File Encoding         : 65001

Date: 2018-09-08 21:30:53
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for `security_authority`
-- ----------------------------
DROP TABLE IF EXISTS `security_authority`;
CREATE TABLE `security_authority` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `authority` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of security_authority
-- ----------------------------
INSERT INTO `security_authority` VALUES ('1', 'ROLE_ADMIN');
INSERT INTO `security_authority` VALUES ('2', 'ROLE_USER');
INSERT INTO `security_authority` VALUES ('3', 'ROLE_BOSS');

-- ----------------------------
-- Table structure for `security_user`
-- ----------------------------
DROP TABLE IF EXISTS `security_user`;
CREATE TABLE `security_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  `email` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of security_user
-- ----------------------------
INSERT INTO `security_user` VALUES ('1', 'admin', '$2a$10$AC0nr/qvRHHn2YNsKNbH/.X9f0dHhIZX0457mwFPBJ6jS2/Tcmu3S', '412444321@qq.com');
INSERT INTO `security_user` VALUES ('2', 'wys', '$2a$10$YH9HijmebwcDfTdbx5ho2OlJ6.zewxufvCrnioVGI5PcXFsqNtCd6', '41fasd321@qq.com');
INSERT INTO `security_user` VALUES ('3', 'boss', '$2a$10$iZ/467THEoA7E/MjOA6iJeBpZJpebIfRzvFbZhKNKwyyFfBypmQTi', 'asdfa@qq.com');

-- ----------------------------
-- Table structure for `security_user_role`
-- ----------------------------
DROP TABLE IF EXISTS `security_user_role`;
CREATE TABLE `security_user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `role_id` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of security_user_role
-- ----------------------------
INSERT INTO `security_user_role` VALUES ('1', '1', '1');
INSERT INTO `security_user_role` VALUES ('2', '2', '2');
INSERT INTO `security_user_role` VALUES ('3', '3', '1');
INSERT INTO `security_user_role` VALUES ('4', '3', '2');
INSERT INTO `security_user_role` VALUES ('5', '3', '3');
INSERT INTO `security_user_role` VALUES ('6', '1', '2');
user_role.sql

  3.2 创建实体类

    技巧01:利用IDEA批量创建实体类,参考文档

  3.4 创建对应的Repository

    每个实体类创建一个Repository

    技巧01:利用Repository添加用户,添加用户时必须利用PasswordEncoder对密码进行加密

    技巧02:添加用户前需要创建一个PasswordEncoder的Bean

  3.5  创建服务层

    技巧01:本博文只创建了UserService的服务层,因为仅仅是实现认证和授权的功能

    技巧02:如果需要实现自定义的认证逻辑,就必须让UserService实现UserDetailsService接口,所有认证相关的逻辑都在loadUserByUsername方法中进行

       技巧03:loadUserByUsername方法中主要是根据用户名查找用户信息以及该用户的角色信息,该方法的返回值是SpringSecurity提供的User对象

package com.xunyji.springsecurity02.service;

import com.xunyji.springsecurity02.pojo.dataobject.SecurityAuthorityDO;
import com.xunyji.springsecurity02.pojo.dataobject.SecurityUserDO;
import com.xunyji.springsecurity02.pojo.dataobject.SecurityUserRoleDO;
import com.xunyji.springsecurity02.repository.AuthorityRepository;
import com.xunyji.springsecurity02.repository.UserRepository;
import com.xunyji.springsecurity02.repository.UserRoleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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 java.util.List;
import java.util.stream.Collectors;

/**
 * @author 王杨帅
 * @create 2018-09-08 20:46
 * @desc
 **/
@Service
public class UserService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private UserRoleRepository userRoleRepository;

    @Autowired
    private AuthorityRepository authorityRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 根据用户名查找用户信息
        SecurityUserDO byUsername = userRepository.findByUsername(username);
        System.out.println(byUsername);

        // 根据用户ID查找用户角色关联信息
        List<SecurityUserRoleDO> byUserId = userRoleRepository.findByUserId(byUsername.getId());
        byUserId.stream().forEach(System.out::println);

        // 获取该用户对应的所有角色的ID列表
        List<Integer> collect = byUserId.stream()
                .map(info -> info.getRoleId())
                .collect(Collectors.toList());
        collect.stream().forEach(System.out::println);

        // 获取该用户对应的所有角色信息
        List<SecurityAuthorityDO> allById = authorityRepository.findAllById(collect);
        allById.stream().forEach(System.out::println);

        List<SimpleGrantedAuthority> authorityList = allById.stream()
                .map(info -> new SimpleGrantedAuthority(info.getAuthority()))
                .collect(Collectors.toList());
        authorityList.stream().forEach(System.out::println);

        // 封装该用户的用户名、密码、角色
        return new User(byUsername.getUsername(), byUsername.getPassword(), authorityList);
    }
}
UserService.java

  3.6 SpringSecurity相关配置

    3.6.1 认证成功拦截器

      认证成功后默认是跳转到之前的请求API,可以通过认证成功拦截器让认证成功后返回一些JSON信息

package com.xunyji.springsecurity02.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

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

/**
 * @author 王杨帅
 * @create 2018-09-08 15:19
 * @desc
 **/
@Component
@Slf4j
public class XiangXuAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        log.info("登录成功");
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.getWriter()
                                .write(objectMapper.writeValueAsString(authentication));
    }
}
XiangXuAuthenticationSuccessHandler.java

    3.6.2 认证失败拦截器

      认证失败后默认是跳转到 /login ,可以通过认证失败拦截器让认证失败后返回一些JSON信息

package com.xunyji.springsecurity02.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * @author 王杨帅
 * @create 2018-09-08 15:22
 * @desc
 **/
@Component
@Slf4j
public class XiangXuAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        log.info("登陆失败");

        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(exception));
    }
}
XiangXuAuthenticationFailureHandler.java

     3.6.2 自定义认证授权配置

package com.xunyji.springsecurity02.config;

import com.xunyji.springsecurity02.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

/**
 * @author 王杨帅
 * @create 2018-09-08 20:13
 * @desc
 **/
@Configuration
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserService userService;

    @Autowired
    private AuthenticationFailureHandler xiangXuAuthenticationFailureHandler;

    @Autowired
    private AuthenticationSuccessHandler xiangXuAuthenticationSuccessHandler;


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider()); // 添加自定义的认证逻辑
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .formLogin()
                .loginProcessingUrl("/furyLogin") // 提交登录信息的API
                .usernameParameter("name") // 登录名
                .passwordParameter("pwd") // 登录密码
                .successHandler(xiangXuAuthenticationSuccessHandler) // 登录成功处理器
                .failureHandler(xiangXuAuthenticationFailureHandler) // 登录失败处理器
            .and().authorizeRequests()
                .antMatchers("/test/home").permitAll() // 该api不需要授权
                .anyRequest().authenticated() // 剩余都需要授权
            .and()
                .logout().permitAll() // 登出API不需要授权
            .and()
                .csrf().disable();

    }

    /**
     * 創建PsswordEncoder對應的Bean
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 创建认证提供者Bean
     * 技巧01:DaoAuthenticationProvider是SpringSecurity提供的AuthenticationProvider实现类
     * @return
     */
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider
                = new DaoAuthenticationProvider(); // 创建DaoAuthenticationProvider实例
        authProvider.setUserDetailsService(userService); // 将自定义的认证逻辑添加到DaoAuthenticationProvider
        authProvider.setPasswordEncoder(passwordEncoder); // 设置自定义的密码加密
        return authProvider;
    }

}
MySpringSecurityConfig.java

  3,7 编写控制类

package com.xunyji.springsecurity02.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 王杨帅
 * @create 2018-09-08 21:17
 * @desc
 **/
@RestController
@RequestMapping(value = "/test")
@Slf4j
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启授权
public class TestController {

    @GetMapping(value = "/home")
    public String home() {
        String info = "寻渝记主页面";
        log.info(info);
        return info;
    }

    @PreAuthorize("hasRole('ROLE_BOSS')") // 拥有ROLE_BOSS角色的用户可以访问
    @GetMapping(value = "/boss")
    public String boss() {
        String info = "拥有ROLE_BOSS权限的才可以进入";
        log.info(info);
        return info;
    }

    @PreAuthorize("hasRole('ROLE_BOSS') OR hasRole('ROLE_ADMIN')")
    @GetMapping(value = "/admin")
    public String admin() {
        String info = "拥有ROLE_ADMIN权限的才可以进入";
        log.info(info);
        return info;
    }

    @PreAuthorize("hasRole('ROLE_BOSS') OR hasRole('ROLE_ADMIN') OR hasRole('ROLE_USER')")
    @GetMapping(value = "/user")
    public String user() {
        String info = "拥有ROLE_USER权限的才可以进入";
        log.info(info);
        return info;
    }

}
TestController.java

  3.8 启动测试

    技巧01:如果没有进行认证前访问需要进行权限的 Restful 服务,就会自动返回一个登录页面【问题:如何返回一个JSON信息呢】

    技巧02:登录认证成功后,如果访问该用户权限之外的 Restful 服务,那么就会提示权限不足【即:403状态码】

 

4 本博文源代码

  点击获取

 

 

 

 

posted @ 2017-12-06 16:03  寻渝记  阅读(1322)  评论(0编辑  收藏  举报