SpringSecurity实现单体应用的认证授权

2020-06-27  本文已影响0人  攻城老狮

技术栈:SpringBoot + Mybatis + SpringSecurity
程序地址:https://github.com/yaokuku123/spring-security
功能:实现基础的访问控制功能。通过注册在数据库中的用户,角色信息,实现认证和授权的功能

1. 目录结构

image-20210224104648367.png

2. MySQL建表

2.1 构建MySQL数据库

/*
SQLyog Ultimate v12.08 (64 bit)
MySQL - 8.0.16 : Database - security_authority
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
/*Table structure for table `sys_permission` */

DROP TABLE IF EXISTS `sys_permission`;

CREATE TABLE `sys_permission` (
  `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `permission_NAME` varchar(30) DEFAULT NULL COMMENT '菜单名称',
  `permission_url` varchar(100) DEFAULT NULL COMMENT '菜单地址',
  `parent_id` int(11) NOT NULL DEFAULT '0' COMMENT '父菜单id',
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `sys_permission` */

/*Table structure for table `sys_role` */

DROP TABLE IF EXISTS `sys_role`;

CREATE TABLE `sys_role` (
  `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `ROLE_NAME` varchar(30) DEFAULT NULL COMMENT '角色名称',
  `ROLE_DESC` varchar(60) DEFAULT NULL COMMENT '角色描述',
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

/*Data for the table `sys_role` */

/*Table structure for table `sys_role_permission` */

DROP TABLE IF EXISTS `sys_role_permission`;

CREATE TABLE `sys_role_permission` (
  `RID` int(11) NOT NULL COMMENT '角色编号',
  `PID` int(11) NOT NULL COMMENT '权限编号',
  PRIMARY KEY (`RID`,`PID`),
  KEY `FK_Reference_12` (`PID`),
  CONSTRAINT `FK_Reference_11` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`),
  CONSTRAINT `FK_Reference_12` FOREIGN KEY (`PID`) REFERENCES `sys_permission` (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `sys_role_permission` */

/*Table structure for table `sys_user` */

DROP TABLE IF EXISTS `sys_user`;

CREATE TABLE `sys_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) NOT NULL COMMENT '用户名称',
  `password` varchar(120) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
  `status` int(1) DEFAULT '1' COMMENT '1开启0关闭',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

/*Data for the table `sys_user` */

/*Table structure for table `sys_user_role` */

DROP TABLE IF EXISTS `sys_user_role`;

CREATE TABLE `sys_user_role` (
  `UID` int(11) NOT NULL COMMENT '用户编号',
  `RID` int(11) NOT NULL COMMENT '角色编号',
  PRIMARY KEY (`UID`,`RID`),
  KEY `FK_Reference_10` (`RID`),
  CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`),
  CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `sys_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `sys_user_role` */

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

2.2 在MySQL的表中添加几组数据用于测试

说明:

# sys_user表 password使用加密的方式存储 两者的密码明文为 199748
+----+----------+--------------------------------------------------------------+--------+
| id | username | password                                                     | status |
+----+----------+--------------------------------------------------------------+--------+
|  4 | xiaoming | $2a$10$9fSu8H/o/qKhRYm8N3IrGePdu5Kj3QNujaW5whHGyoi8ta0Bj4SSG |      1 |
|  5 | xiaoma   | $2a$10$NJkRs/2AoD1iHlLNe4LwPu8M1ZvmVp4lsCD0QEqCoaRg1Jn2AG2hu |      1 |
+----+----------+--------------------------------------------------------------+--------+

# sys_role表
+----+--------------+--------------+
| ID | ROLE_NAME    | ROLE_DESC    |
+----+--------------+--------------+
|  6 | ROLE_USER    | Basic Role   |
|  7 | ROLE_PRODUCT | Product Role |
|  8 | ROLE_ORDER   | Order Role   |
|  9 | ROLE_ADMIN   | Root         |
+----+--------------+--------------+

# sys_user_role表
+-----+-----+
| UID | RID |
+-----+-----+
|   4 |   6 |
|   5 |   6 |
|   4 |   7 |
|   5 |   8 |
+-----+-----+

3. 页面部分说明

3.1 templates文件夹存放使用Thymeleaf模板引擎的动态资源文件

  1. index.html,默认登录认证成功后的页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8"/>
    <title>Spring Security</title>
</head>

<body>
   <h1>Success</h1>
   <a th:href="@{/product}">产品资源</a><br>
   <a th:href="@{/order}">订单资源</a>
   <form th:action="@{/logout}" method="post">
       <input type="submit" value="注销"/>
   </form>
</body>

</html>
  1. product.html,产品资源文件,授权给拥有ROLE_PRODUCT角色的用户
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8"/>
    <title>product</title>
</head>

<body class="container">
    <h1 th:text="产品资源">product</h1>
    <a th:href="@{/}">返回</a>
</body>

</html>
  1. order.html,订单资源文件,授权给拥有ROLE_ORDER角色的用户
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8"/>
    <title>product</title>
</head>

<body class="container">
    <h1 th:text="订单资源">order</h1>
    <a th:href="@{/}">返回</a>
</body>

</html>

3.2 static文件夹中存放的静态资源文件

  1. 403.html 403错误页面,用于处理来自403权限不足错误的请求
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>403</title>
</head>
<body>
    <h1>403 Error</h1>
    <a href="/">返回</a>
</body>
</html>
  1. 500.html 500错误页面,用于简单处理其他错误的页面
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>500</title>
</head>
<body>
    <h1>500 Error</h1>
    <a href="/">返回</a>
</body>
</html>

4. POM文件,相关依赖

<?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.yqj</groupId>
    <artifactId>springboot-springsecurity</artifactId>
    <version>1.0-SNAPSHOT</version>

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

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

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

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

</project>

5. yaml文件完成Spring的相关配置

application.yaml文件

server:
  port: 8080

spring:
  datasource:
    username: root
    password: 199748
    url: jdbc:mysql:///security_authority
    driver-class-name: com.mysql.jdbc.Driver

mybatis:
  type-aliases-package: com.yqj.domain
  configuration:
    map-underscore-to-camel-case: true

logging:
  level:
    com.yqj: debug

6. 业务逻辑相关内容的编写

6.1 SpringBoot启动类

SecurityApplication文件

package com.yqj;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Copyright(C),2019-2021,XXX公司
 * FileName: SecurityApplication
 * Author: yaoqijun
 * Date: 2021/2/24 10:13
 */
@SpringBootApplication
@MapperScan("com.yqj.mapper")
public class SecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class,args);
    }
}

6.2 用户和角色类 com.yqj.springsecurity.domain

  1. SysUser用户类
package com.yqj.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

/**
 * Copyright(C),2019-2021,XXX公司
 * FileName: SysUser
 * Author: yaoqijun
 * Date: 2021/2/24 10:14
 */
@Data
public class SysUser implements UserDetails {

    private Integer id;
    private String username;
    private String password;
    private List<SysRole> roles;

    @JsonIgnore
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

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

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

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

    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return true;
    }
}
  1. SysRole角色类
package com.yqj.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;

/**
 * Copyright(C),2019-2021,XXX公司
 * FileName: SysRole
 * Author: yaoqijun
 * Date: 2021/2/24 10:15
 */
@Data
public class SysRole implements GrantedAuthority {

    private Integer id;
    private String roleName;
    private String roleDesc;

    @JsonIgnore
    @Override
    public String getAuthority() {
        return roleName;
    }
}

6.3 数据库访问层 com.yqj.springsecurity.mapper

  1. UserMapper,用户访问层接口
package com.yqj.mapper;

import com.yqj.domain.SysUser;
import org.apache.ibatis.annotations.Many;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
 * Copyright(C),2019-2021,XXX公司
 * FileName: UserMapper
 * Author: yaoqijun
 * Date: 2021/2/24 10:17
 */
public interface UserMapper {

    @Select("select * from sys_user where username=#{username}")
    @Results({
            @Result(id = true, property = "id", column = "id"),
            @Result(property = "roles", column = "id", javaType = List.class,
                    many = @Many(select = "com.yqj.mapper.RoleMapper.findByUid"))
    })
    public SysUser findByName(String username);
}
  1. RoleMapper,角色访问层接口
package com.yqj.mapper;

import com.yqj.domain.SysRole;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
 * Copyright(C),2019-2021,XXX公司
 * FileName: RoleMapper
 * Author: yaoqijun
 * Date: 2021/2/24 10:18
 */
public interface RoleMapper {

    @Select(" select r.id,r.role_name roleName,r.role_desc roleDesc " +
            " from sys_role r,sys_user_role ur " +
            " where r.id=ur.rid and ur.uid=#{uid} ")
    public List<SysRole> findByUid(Integer uid);
}

6.4 服务层 com.yqj.springsecurity.service

  1. UserService,继承SpringSecurity中的用于认证的类UserDetailsService
package com.yqj.service;

import org.springframework.security.core.userdetails.UserDetailsService;

/**
 * Copyright(C),2019-2021,XXX公司
 * FileName: UserService
 * Author: yaoqijun
 * Date: 2021/2/24 10:18
 */
public interface UserService extends UserDetailsService {
}
  1. UserServiceImpl,接口的实现类
package com.yqj.service.impl;

import com.yqj.mapper.UserMapper;
import com.yqj.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * Copyright(C),2019-2021,XXX公司
 * FileName: UserServiceImpl
 * Author: yaoqijun
 * Date: 2021/2/24 10:19
 */
@Service
@Transactional
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userMapper.findByName(username);
    }
}

6.5 控制层 com.yqj.springsecurity.controller

SecurityController,用于对请求路径进行转发,从而可以访问由模板引擎渲染的动态资源

package com.yqj.controller;

import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * Copyright(C),2019-2021,XXX公司
 * FileName: SecurityController
 * Author: yaoqijun
 * Date: 2021/2/24 10:20
 */
@Controller
public class SecurityController {

    @RequestMapping("/")
    public String login() {
        return "index";
    }

    @Secured("ROLE_PRODUCT")
    @RequestMapping("/product")
    public String product() {
        return "product";
    }

    @Secured("ROLE_ORDER")
    @RequestMapping("/order")
    public String learning() {
        return "order";
    }

}

6.6 配置类

  1. SecurityConfig,配置SpringSecurity相关内容
package com.yqj.config;

import com.yqj.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.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.bcrypt.BCryptPasswordEncoder;

/**
 * Copyright(C),2019-2021,XXX公司
 * FileName: SecurityConfig
 * Author: yaoqijun
 * Date: 2021/2/24 10:21
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/**").hasAnyRole("USER","ADMIN")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/login")
                .successForwardUrl("/")
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout")
                .invalidateHttpSession(true)
                .permitAll()
                .and()
                .csrf().disable();
    }
}
  1. ControllerExceptionHandler,用于处理错误请求的情况
package com.yqj.controller;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;


/**
 * Copyright(C),2019-2021,XXX公司
 * FileName: ControllerExceptionHandler
 * Author: yaoqijun
 * Date: 2021/2/24 10:26
 */
@ControllerAdvice
public class ControllerExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    public String handlerException(RuntimeException e){
        if(e instanceof AccessDeniedException){
            //重定向到静态页面
            return "redirect:/403.html";
        }else {
            return "redirect:/500.html";
        }
    }
}

7 效果说明

上一篇 下一篇

猜你喜欢

热点阅读