SpringSecurity学习总结

第一、SpringSecurity-简介

1.1简介

SpringSecurity融合Spring技术栈,提供JavaEE应 用的整体安全解决方案;

Spring Security为基于Java EE的企业软件应用提供全面的安全服务。

Spring Security只需要少量配置,就能构建一个强大的安全的应用系统。  

目前市面上受欢迎的两个安全框架:Apache Shiro、SpringSecurity;

SpringSecurity可以无缝整合Spring应用,具有强大的自动化web安全管控功能。而Shiro是一个轻量级强大的安全框架,可以脱离web应用来提供安全管控,但是对于web的一些定制安全需要手动编写;SpringBoot底层默认整合SpringSecurity作为安全框架,所以我们推荐web应用使用SpringSecurity来控制安全;

1.2概念

1.2.1认证

authentication:身份验证

   “身份验证”是指建立主体(principal)的过程,主体就是他们声称是谁(“主体”通常指用户、设备或在应用程序中可以执行动作的其他系统)。也就是“证明你是谁”

1.2.2授权

authorization:授权

    “授权”是指确定主体(principal)是否被允许执行系统中某个动作的过程。 也就是“你能做什么!”

 为了达到“授权”决策(安全框架决定你是否有权限做此事),“身份验证”(authentication)过程已经建立了主体的身份(Principal)

1.2.3支持的身份认证模式

在身份验证级别,Spring Security支持广泛的认证模型。这些认证模型中的大部分要么由第三方提供,要么由相关标准机构(如互联网工程任务组)开发。此外,Spring Security提供了自己的一套身份验证功能。具体而言,Spring Security当前支持与所有这些技术的身份验证集成;

HTTP BASIC身份验证标头
HTTP BASIC authentication headers (an IETF RFC-based standard)
参考:https://blog.csdn.net/lvxinzhi/article/details/49000003
HTTP Digest身份验证标头
HTTP Digest authentication headers (an IETF RFC-based standard)
HTTP X.509客户端证书交换
HTTP X.509 client certificate exchange (an IETF RFC-based standard)
LDAP(一种非常常见的跨平台身份验证方法,特别是在大型环境中)
LDAP (a very common approach to cross-platform authentication needs, especially in large environments)
基于表单的身份验证(用于简单的用户界面需求)
Form-based authentication (for simple user interface needs)
OpenID身份验证
OpenID authentication
基于预先建立的请求标头的身份验证
Authentication based on pre-established request headers (such as Computer Associates Siteminder)
Jasig中央认证服务(也称为CAS,是一种流行的开源单点登录系统)
Jasig Central Authentication Service (otherwise known as CAS, which is a popular open source single sign-on system)
远程方法调用(RMI)和HttpInvoker(Spring远程协议)的透明身份验证上下文传播
Transparent authentication context propagation for Remote Method Invocation (RMI) and HttpInvoker (a Spring remoting 
protocol) 自动“记住我”身份验证 Automatic
"remember-me" authentication (so you can tick a box to avoid re-authentication for a predetermined period of
time) 匿名身份验证(允许每个未经身份验证自动承担特定的安全身份) Anonymous authentication (allowing every unauthenticated call to automatically assume a particular security identity) Runas身份验证(如果一个调用应继续使用不同的安全标识,则非常有用) Run
-as authentication (which is useful if one call should proceed with a different security identity) Java身份验证和授权服务(JAAS) Java Authentication and Authorization Service (JAAS) JavaEE容器身份验证(如果需要,您仍然可以使用容器管理身份验证) Java EE container authentication (so you can still use Container Managed Authentication if desired) Java开源单点登录(JOSSO) Java Open Source Single Sign-On (JOSSO) * OpenNMS网络管理平台* OpenNMS Network Management Platform * 您自己的身份验证系统 Your own authentication systems (see below) 其他 Kerberos AppFuse * AndroMDA * Mule ESB * Direct Web Request (DWR) * Grails * Tapestry * JTrac * Jasypt * Roller * Elastic Path * Atlassian Crowd *

1.2.4 4种使用方式

一种是全部利用配置文件,将用户、权限、资源(url)硬编码在xml文件中

二种是用户和权限用数据库存储,而资源(url)和权限的对应采用硬编码配置

三种是细分角色和权限,并将用户、角色、权限和资源均采用数据库存储,并且自定义过滤器,代替原有的FilterSecurityInterceptor过滤器, 并分别实现AccessDecisionManagerInvocationSecurityMetadataSourceServiceUserDetailsService,并在配置文件中进行相应配置。

四是修改springsecurity的源代码,主要是修改InvocationSecurityMetadataSourceServiceUserDetailsService两个类。

 InvocationSecurityMetadataSourceService

    将配置文件或数据库中存储的资源(url)提取出来加工成为url和权限列表的MapSecurity使用

UserDetailsService

    提取用户名和权限组成一个完整的(UserDetails)User对象,该对象可以提供用户的详细信息供AuthentationManager进行认证与授权使用

第二、SpringBoot整合SpringSecurity-HelloWorld

2.1创建一个maven工程

pom.xml依赖

   <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.6.RELEASE</version>
    </parent>
    <dependencies>
        <!-- SpringBoot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.8</version>
        </dependency>
        <!--thymeleaf依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- springboot整合freemarker -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
            <version>2.1.8.RELEASE</version>
        </dependency>

        <!-->spring-boot 整合security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>

2.2导入静态页面

 2.3创建一个controller

@Controller
public class IndexController {

    private final String PREFIX = "pages/";
    /**
     * 欢迎页
     * @return
     */
    @GetMapping("/")
    public String index() {
        return "welcome";
    }

    /**
     * 登陆页
     * @return
     */
    @GetMapping("/userlogin")
    public String loginPage() {
        return PREFIX+"login";
    }


    /**
     * level1页面映射
     * @param path
     * @return
     */
    @GetMapping("/level1/{path}")
    public String level1(@PathVariable("path")String path) {
        return PREFIX+"level1/"+path;
    }

    /**
     * level2页面映射
     * @param path
     * @return
     */
    @GetMapping("/level2/{path}")
    public String level2(@PathVariable("path")String path) {
        return PREFIX+"level2/"+path;
    }

    /**
     * level3页面映射
     * @param path
     * @return
     */
    @GetMapping("/level3/{path}")
    public String level3(@PathVariable("path")String path) {
        return PREFIX+"level3/"+path;
    }
}

2.4创建一个启动类

@SpringBootApplication
public class SpringSecurityBootstrap {
    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityBootstrap.class);
    }
}

2.5启动测试效果

1.所有资源访问受限(包括静态资源)

2. 需要一个默认的登录页面(框架自带的)

3.账号密码错误会有提示

4.查看登录页面的源码,发现有个hidden-input;name="_csrf" 这是SpringSecurity帮我们防止“跨站请求伪造”攻击;还可以防止表单重复提交。

http://localhost:8080/login?error

第三、SpringSecurity-实验

3.1基于内存的安全登录和授权

/**
 * Security配置类
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 认证
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("zhangsan")
                .password(new BCryptPasswordEncoder().encode("123"))
                .roles("VIP1");

    }
    /**
     * 授权
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //设置匹配的资源放行
        http.authorizeRequests().antMatchers("/","/asserts/**","/pages/login.html","/userlogin")
                .permitAll()
//定制请求的权限规则 .antMatchers(
"/level1/**").hasRole("VIP1") .antMatchers("/level2/**").hasRole("VIP2") .antMatchers("/level3/**").hasRole("VIP3") .anyRequest().authenticated();//剩余任何资源必须认证 //自动开启登录页,效果,如果没有登录,没有权限就会来到登录页面
     //1./login来到登录页 2.重定向到/login?error表示登录失败3.默认post形式的,/login代表处理登录5.一但定制loginPage;那么loginPage
   的post请求就是登录
http.formLogin(); http.csrf().disable(); } }

测试结果:

 zhangsan可以访问/level1/**这个下面的路径,其他路径没有权限访问,报出如下错误:

 3.2注销

添加注销功能(logout)http.logout()默认规则

1) /logout:退出系统

2) 如果csrf开启,必须post方式的/logout请求,表单中需要增加csrf token

3) logoutUrl();退出系统需要发送的请求

4) logoutSuccessUrl();退出系统成功以后要跳转的页面地址

5) addLogoutHandler():自定义注销处理器

6) deleteCookies():指定需要删除的cookie

7) invalidateHttpSession():session失效(DEBUG)

代码实现:

/**
 * Security配置类
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 认证
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("zhangsan")
                .password(new BCryptPasswordEncoder().encode("123"))
                .roles("VIP1");

    }
    /**
     * 授权
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //设置匹配的资源放行
        http.authorizeRequests().antMatchers("/","/asserts/**","/pages/login.html","/userlogin")
                .permitAll()
                .antMatchers("/level1/**").hasRole("VIP1")
                .antMatchers("/level2/**").hasRole("VIP2")
                .antMatchers("/level3/**").hasRole("VIP3")
                .anyRequest().authenticated();//剩余任何资源必须认证
        //自动开启登录页
        http.formLogin();
        //开启自动配置的注销功能
        // 1、访问 /logout 表示用户注销,清空session
          //2、注销成功会返回 /login?logout 页面;
        http.logout().logoutSuccessUrl("/login");//注销之后来到登录页
        http.csrf().disable();
    }
}

在前端页面加上这样一个按钮
<form action="/logout" method="post">
    <input type="submit" value="注销"/>
</form>

 测试结果:

点击了注销按钮,就跳转了登录页面

 

 3.3开启记住我功能

 1. http.rememberMe();

 2.默认规则:

        1.页面checkbox提交remember-me参数

        2. 默认记住2周登录状态:AbstractRememberMeServices

 3.会在cookie中保存名为:remember-me的cookie

4.记住了以前登录的状态,以后再访问就不用登录了

 5. 登录后页面,关闭浏览器,直接访问地址,可以访问成功,不必登录,

6.这种方式,token值是放置在内存中的,服务器端重启tomcat,token会失效。需要将token记录在数据库持久化才不会失效。

 

 7.代码测试

/**
 * Security配置类
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 认证
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("zhangsan")
                .password(new BCryptPasswordEncoder().encode("123"))
                .roles("VIP1");

    }
    /**
     * 授权
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //设置匹配的资源放行
        http.authorizeRequests().antMatchers("/","/asserts/**","/pages/login.html","/userlogin")
                .permitAll()
                .antMatchers("/level1/**").hasRole("VIP1")
                .antMatchers("/level2/**").hasRole("VIP2")
                .antMatchers("/level3/**").hasRole("VIP3")
                .anyRequest().authenticated();//剩余任何资源必须认证
        //自动开启登录页
        http.formLogin();
        //开启自动配置的注销功能
        // 1、访问 /logout 表示用户注销,清空session
          //2、注销成功会返回 /login?logout 页面;
        http.logout().logoutSuccessUrl("/login");//注销之后来到登录页
        //开启记住我功能
        http.rememberMe().rememberMeParameter("remeber");
        //登陆成功以后,将cookie发给浏览器保存,以后访问页面带上这个cookie,只要通过检查就可以免登录
        //点击注销会删除cookie
        http.csrf().disable();
    }
}

启动程序,打开浏览器,到登录页面,输入用户名和密码,并且打钩复选框

测试效果:登录后页面,关闭浏览器,直接访问地址,可以访问成功,不必登录

3.4自定义登录功能

1.表单提交地址:http.formLogin().loginPage("/userlogin");

2.表单提交请求方式:post

3.表单提交请求失败,提取的错误信息:{SPRING_SECURITY_LAST_EXCEPTION.message}

4.提交表单的参数:   username  password暂时禁用csrf:http.csrf().disable();

5.代码案例:

/**
 * Security配置类
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 认证
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("zhangsan")
                .password(new BCryptPasswordEncoder().encode("123"))
                .roles("VIP1");

    }
    /**
     * 授权
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //设置匹配的资源放行
        http.authorizeRequests().antMatchers("/","/asserts/**","/pages/login.html","/userlogin")
                .permitAll()
                .antMatchers("/level1/**").hasRole("VIP1")
                .antMatchers("/level2/**").hasRole("VIP2")
                .antMatchers("/level3/**").hasRole("VIP3")
                .anyRequest().authenticated();//剩余任何资源必须认证
        //自动开启登录页
        http.formLogin().loginPage("/userlogin");
        //开启自动配置的注销功能
        // 1、访问 /logout 表示用户注销,清空session
          //2、注销成功会返回 /login?logout 页面;
        http.logout().logoutSuccessUrl("/userlogin");//注销之后来到登录页
        //开启记住我功能
        http.rememberMe().rememberMeParameter("remeber");
        //登陆成功以后,将cookie发给浏览器保存,以后访问页面带上这个cookie,只要通过检查就可以免登录
        //点击注销会删除cookie
        http.csrf().disable();
    }
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
    <script type="text/javascript" src="asserts/js/jquery-2.1.1.min.js"></script>
</head>
<body>
    <h1 align="center">欢迎登陆武林秘籍管理系统</h1>
    <hr>
    <div align="center">
        <form action="/userlogin" method="post" id="form">
            用户名:<input name="username"/><br>
            密码:<input name="password"><br/>
            <input type="button" value="登陆" id="formBtn"/>
        </form>
    </div>
    <script>
        $(function () {
            $('#formBtn').click(function () {
                $('#form').submit();
            });
        })
    </script>
</body>
</html>

测试效果:访问一个受保护的路径会自动跳转到自定义的登录页面,如图所示:

6.登录逻辑分析

/**默认登录页面

   * /login GET - the login form

   * /login POST - process the credentials and if valid authenticate the user

   * /login?error GET - redirect here for failed authentication attempts

   * /login?logout GET - redirect here after successfully logging out

   * 定制登录页面:loginPage("/index.jsp"):规定登录页的地址在哪里

   * /index.jsp GET - the login form

   * /index.jsp POST - process the credentials and if valid authenticate the user

   * /index.jsp?error  GET - redirect here for failed authentication attempts

   * /index.jsp?logout GET - redirect here after successfully logging out

* ${SPRING_SECURITY_LAST_EXCEPTION.message}可以取出错误消息

   */

 7.注意事项

    1. CSRF跨站请求伪造

  2.SpringSecurity添加了csrf功能【DefaultCsrfToken】,所有的表单提交为了防止跨站请求伪造,我们需要加上_csrf项; 或者,暂时禁用http.csrf().disable();

        <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> 例如:<input type="hidden" name="_csrf" value="310988c2-3f9d-4651-9e19-6ef4b2c4aa3a"/>  如果不禁用csrf,默认是开启的状态;页面不设置csrf表单域,那么,提交登录请求会出现不能登录的问题

禁用CSRF:http.csrf().disable();

领牌值变化:

1. 如果登录成功(用户名,密码正确),令牌会被删除,

2.重新回到登录页或后退网页,令牌会重新生成;

3.如果登录失败(用户名,密码错误),令牌不变。

4. 刷新登录页,令牌值也不变

作用:防止别的网站伪造数据,提交请求到我们的网站。

3.5自定义异常处理

1. 直接增加处理异常界面:http.exceptionHandling().accessDeniedPage("/error403");

2.在控制器类中增加映射处理

 @GetMapping("/error403")
    public String error403() {
        return "error403";
    }

3.增加显示页面,将welcome.html复制,命名为error403.html,增加一句提示信息

 4.自定义异常处理器

http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
     AccessDeniedException accessDeniedException) throws IOException, ServletException {
     request.setAttribute("message", accessDeniedException.getMessage());
     request.getRequestDispatcher("/error403").forward(request, response);
}
});

 3.5基于数据库的验证

 3.5.1环境搭建

maven依赖

  <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.6.RELEASE</version>
    </parent>

    <dependencies>
        <!-- SpringBoot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.8</version>
        </dependency>
        <!--thymeleaf依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- springboot整合freemarker -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
            <version>2.1.8.RELEASE</version>
        </dependency>

        <!-->spring-boot 整合security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- springboot 整合mybatis框架 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- alibaba的druid数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.9</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

 yml配置文件

spring:
  thymeleaf:
    cache: false
    mode: HTML5
    prefix: classpath:/templates/
    suffix: .html
    ####\u6574\u5408\u6570\u636E\u5E93\u5C42
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test
    username: root
    password: root
    # druid \u8FDE\u63A5\u6C60
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver

 MD5工具类

public class MD5Util {

    private static final String SALT = "yehui";

    public static String encode(String password) {
        password = password + SALT;
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        char[] charArray = password.toCharArray();
        byte[] byteArray = new byte[charArray.length];

        for (int i = 0; i < charArray.length; i++)
            byteArray[i] = (byte) charArray[i];
        byte[] md5Bytes = md5.digest(byteArray);
        StringBuffer hexValue = new StringBuffer();
        for (int i = 0; i < md5Bytes.length; i++) {
            int val = ((int) md5Bytes[i]) & 0xff;
            if (val < 16) {
                hexValue.append("0");
            }

            hexValue.append(Integer.toHexString(val));
        }
        return hexValue.toString();
    }
}

 启动类:

@SpringBootApplication
public class SpringSecurityBootstrap {
    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityBootstrap.class);
    }
}

 数据库sql:

DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '名称',
  `code` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '编码',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `code_UNIQUE`(`code`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 328 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '权限表' ROW_FORMAT = Compact;

INSERT INTO `sys_permission` VALUES (1, '用户查询', 'ROLE_UC_USERS_SELECT');
INSERT INTO `sys_permission` VALUES (2, '组织机构查询', 'ROLE_UC_ORGS_SELECT');
INSERT INTO `sys_permission` VALUES (3, '员工查询', 'ROLE_UC_STAFFS_SELECT');
INSERT INTO `sys_permission` VALUES (4, '权限查询', 'ROLE_UC_ROLES_SELECT');
INSERT INTO `sys_permission` VALUES (13, '字典查询', 'ROLE_UC_DICTS_SELECT');
INSERT INTO `sys_permission` VALUES (101, '产品分类查询', 'ROLE_CATEGORIES_SELECT');
INSERT INTO `sys_permission` VALUES (103, '产品查询', 'ROLE_PRODUCTS_SELECT');
INSERT INTO `sys_permission` VALUES (107, '生产商查询', 'ROLE_UC_PRODUCERS_SELECT');
INSERT INTO `sys_permission` VALUES (109, '供应商查询', 'ROLE_UC_SELLERS_SELECT');
INSERT INTO `sys_permission` VALUES (111, '客户查询', 'ROLE_UC_CUSTOMERS_SELECT');
INSERT INTO `sys_permission` VALUES (117, '首页图片查询', 'ROLE_UC_INDEX_PICTURES_SELECT');
INSERT INTO `sys_permission` VALUES (119, '文章分类查询', 'ROLE_UC_ARTICLES_SELECT');
INSERT INTO `sys_permission` VALUES (121, '文章查询', 'ROLE_UC_ARTICLE_CATALOGS_SELECT');
INSERT INTO `sys_permission` VALUES (129, '订单查询', 'ROLE_UC_ORDERS_SELECT');
INSERT INTO `sys_permission` VALUES (131, '需求查询', 'ROLE_UC_REQUIREMENTS_SELECT');
INSERT INTO `sys_permission` VALUES (141, '系统参数查询', 'ROLE_UC_PARAMETERS_SELECT');
INSERT INTO `sys_permission` VALUES (144, '今日热点', 'ROLE_UC_HOTSPOT_SELECT');
INSERT INTO `sys_permission` VALUES (158, '头部Banner查询', 'ROLE_UC_BANNER_SELECT');
INSERT INTO `sys_permission` VALUES (160, '活动图片查询', 'ROLE_UC_ACTIVITY_SELECT');
INSERT INTO `sys_permission` VALUES (162, '明星供应商查询', 'ROLE_UC_STARSELLERS_SELECT');
INSERT INTO `sys_permission` VALUES (226, '买家查询', 'ROLE_BUYER_SELECT');
INSERT INTO `sys_permission` VALUES (227, '卖家查询', 'ROLE_SELLERS_SELECT');
INSERT INTO `sys_permission` VALUES (228, '属性查询', 'ROLE_ATTR_SELECT');
INSERT INTO `sys_permission` VALUES (229, '商品查询', 'ROLE_ITEM_SELECT');
INSERT INTO `sys_permission` VALUES (230, '退货查询', 'ROLE_RE_ORDER_SELECT');
INSERT INTO `sys_permission` VALUES (231, '城市查询', 'ROLE_CITY_SELECT');
INSERT INTO `sys_permission` VALUES (232, '区域查询', 'ROLE_DISTRICT_SELECT');
INSERT INTO `sys_permission` VALUES (233, '物流点管理', 'ROLE_LOG_DIS_SELECT');
INSERT INTO `sys_permission` VALUES (234, '物流费用查询', 'ROLE_LOG_EXP_SELECT');
INSERT INTO `sys_permission` VALUES (246, '楼层查询', 'ROLE_FLOOR_SELECT');
INSERT INTO `sys_permission` VALUES (248, '需求查询', 'ROLE_STATISTIC');
INSERT INTO `sys_permission` VALUES (249, ' 流行时尚查询', 'ROLE_DESIGN_SELECT');
INSERT INTO `sys_permission` VALUES (254, '时尚明细查询', 'ROLE_DESIGNDETAIL_SELECT');
INSERT INTO `sys_permission` VALUES (258, 'VR管理查询', 'ROLE_VR_SELECT');
INSERT INTO `sys_permission` VALUES (262, '语言管理查询', 'ROLE_LAN_SELECT');
INSERT INTO `sys_permission` VALUES (268, '线下订单', 'ROLE_ORDERS_SELECT');
INSERT INTO `sys_permission` VALUES (269, '广告管理', 'ROLE_UC_ADVERTISEMENTS_UPDATE');
INSERT INTO `sys_permission` VALUES (295, '角色查询', 'ROLE_SECURITY_SELECT');
INSERT INTO `sys_permission` VALUES (296, '菜单查询', 'ROLE_MENU_SELECT');
INSERT INTO `sys_permission` VALUES (297, '门户查询', 'ROLE_PORTAL_SELECT');
INSERT INTO `sys_permission` VALUES (299, '门户营销查询', 'ROLE_MARKET_SELECT');
INSERT INTO `sys_permission` VALUES (300, '门户商家查询', 'ROLE_xx_SELECT');
INSERT INTO `sys_permission` VALUES (304, '匹配查询', 'ROLE_ss_SELECT');
INSERT INTO `sys_permission` VALUES (305, '推荐查询', 'ROLE_tt_SELECT');
INSERT INTO `sys_permission` VALUES (306, '商户查询', 'ROLE_SELLER_SELECT');
INSERT INTO `sys_permission` VALUES (308, '二级域名查询', 'ROLE_ej_SELECT');
INSERT INTO `sys_permission` VALUES (309, '商户通知查询', 'ROLE_tz_SELECT');
INSERT INTO `sys_permission` VALUES (310, '品牌查询', 'ROLE_BRAND_SELECT');
INSERT INTO `sys_permission` VALUES (311, '仓储查询', 'ROLE_STORAGE_SELECT');
INSERT INTO `sys_permission` VALUES (312, '物流政策查询', 'ROLE_LOG_POLICY_SELECT');
INSERT INTO `sys_permission` VALUES (313, '预售查询', 'ROLE_PRESALE_SELECT');
INSERT INTO `sys_permission` VALUES (314, '互动推广查询', 'ROLE_hd_SELECT');
INSERT INTO `sys_permission` VALUES (315, '账号查询', 'ROLE_BUYERS_SELECT');
INSERT INTO `sys_permission` VALUES (316, '投诉查询', 'ROLE_ts_SELECT');
INSERT INTO `sys_permission` VALUES (317, '客单查询', 'ROLE_kd_SELECT');
INSERT INTO `sys_permission` VALUES (318, '商家销售查询', 'ROLE_xs_SELECT');
INSERT INTO `sys_permission` VALUES (319, '商家菜单查询', 'ROLE_SELLERMENU_SELECT');
INSERT INTO `sys_permission` VALUES (320, '积分管理', 'dsfds');
INSERT INTO `sys_permission` VALUES (321, '22', '22');
INSERT INTO `sys_permission` VALUES (322, '文章维护', 'select');
INSERT INTO `sys_permission` VALUES (323, '栏目分类', '栏目分类');
INSERT INTO `sys_permission` VALUES (324, '1', '1');
INSERT INTO `sys_permission` VALUES (325, 'HELP_CENTER', 'HELP_CENTER');
INSERT INTO `sys_permission` VALUES (326, 'REDIS数据同步', 'REDIS');
INSERT INTO `sys_permission` VALUES (327, '平台主页', 'ROLE_UC_HOMEPAGE_SELECT');

DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `id` int(10) NOT NULL,
  `roleName` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `roleDesc` varchar(50) 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 = Compact;

INSERT INTO `sys_role` VALUES (1, 'admin', '管理员');
INSERT INTO `sys_role` VALUES (2, 'add_user', '添加管理员');


DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission`  (
  `role_id` int(10) NULL DEFAULT NULL,
  `perm_id` int(10) NULL DEFAULT NULL,
  INDEX `FK_Reference_3`(`role_id`) USING BTREE,
  INDEX `FK_Reference_4`(`perm_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;


INSERT INTO `sys_role_permission` VALUES (1, 1);
INSERT INTO `sys_role_permission` VALUES (1, 2);
INSERT INTO `sys_role_permission` VALUES (1, 3);
INSERT INTO `sys_role_permission` VALUES (1, 4);
INSERT INTO `sys_role_permission` VALUES (2, 1);
INSERT INTO `sys_role_permission` VALUES (2, 2);


DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` int(10) NOT NULL,
  `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `realname` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `createDate` date NULL DEFAULT NULL,
  `lastLoginTime` date NULL DEFAULT NULL,
  `enabled` int(5) NULL DEFAULT NULL,
  `accountNonExpired` int(5) NULL DEFAULT NULL,
  `accountNonLocked` int(5) NULL DEFAULT NULL,
  `credentialsNonExpired` int(5) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;


INSERT INTO `sys_user` VALUES (1, 'admin', '张三', 'dc10c7f558ccc4b4819705d6da54a130', '2018-11-13', '2018-11-13', 1, 1, 1, 1);
INSERT INTO `sys_user` VALUES (2, 'userAdd', '小余', 'dc10c7f558ccc4b4819705d6da54a130', '2018-11-13', '2018-11-13', 1, 1, 1, 1);


DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role`  (
  `user_id` int(10) NULL DEFAULT NULL,
  `role_id` int(10) NULL DEFAULT NULL,
  INDEX `FK_Reference_1`(`user_id`) USING BTREE,
  INDEX `FK_Reference_2`(`role_id`) USING BTREE,
  CONSTRAINT `FK_Reference_1` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `FK_Reference_2` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `sys_user_role` VALUES (1, 1);
INSERT INTO `sys_user_role` VALUES (2, 2);

 Mapper接口

@Mapper
public interface UserMapper {
    // 查询用户信息
    @Select(" select * from sys_user where username = #{userName}")
    LoginUser findByUsername(@Param("userName") String userName);

    // 查询用户的权限
    @Select(" select permission.* from sys_user user" + " inner join sys_user_role user_role"
            + " on user.id = user_role.user_id inner join "
            + "sys_role_permission role_permission on user_role.role_id = role_permission.role_id "
            + " inner join sys_permission permission on role_permission.perm_id = permission.id where user.username = #{userName};")
    List<Permission> findPermissionByUsername(@Param("userName") String userName);
    @Select("SELECT a.* FROM sys_role a , sys_user_role b ,sys_user c" +
            " WHERE a.id = b.role_id AND c.id = b.user_id WHERE c.username = #{userName}")
    List<Role> finRoleByUserName(@Param("userName") String userName);
}
@Mapper
public interface PermissionMapper {

    // 查询所有权限
    @Select(" select * from sys_permission ")
    List<Permission> findAllPermission();

}

 实体类:

@Data
public class LoginUser {
    private Integer id;
    private String username;
    private String realname;
    private String password;
    private Date createDate;
    private Date lastLoginTime;
    private boolean enabled = true;
    private boolean accountNonExpired = true;
    private boolean accountNonLocked = true;
    private boolean credentialsNonExpired =true;
}
// 权限表
@Data
public class Permission {
    private Integer id;
    // 权限名称
    private String name;
    // 编码名称
    private String code;

}
// 角色信息表
@Data
public class Role {
    private Integer id;
    private String roleName;
    private String roleDesc;
}

 创建一个controller

@Controller
public class IndexController {

    private final String PREFIX = "pages/";
    /**
     * 欢迎页
     * @return
     */
    @GetMapping("/")
    public String index() {
        return "welcome";
    }
    @GetMapping("/error403")
    public String error403() {
        return "error403";
    }

    /**
     * 登陆页
     * @return
     */
    @GetMapping("/userlogin")
    public String loginPage() {
        return PREFIX+"login";
    }


    /**
     * level1页面映射
     * @param
     * @return
     */
    @GetMapping("/level1/1")
    public String level1() {
        return PREFIX+"level1/"+1;
    }

    /**
     * level2页面映射
     * @param
     * @return
     */
    @GetMapping("/level1/2")
    public String level2() {
        return PREFIX+"level1/"+2;
    }

    /**
     * level3页面映射
     * @param
     * @return
     */
    @GetMapping("/level1/3")
    public String level3() {
        return PREFIX+"level1/"+3;
    }
}

 后台环境搭建完,前端会上传githup上面,结构如图所示

 3.5.2自定义UserDetailsService检索用户

实现UserDetailService接口loadUserByUsername(String username)方法

编写UserDetailService实现:

1)接口及已有实现类

 2)实现UserDetailService接口,提供自定义实现类:

@Component
public class DefaultUserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
                if(StringUtils.isEmpty(userName)){
                    throw new RuntimeException("用户名不能为空");
        }
        //通过用户名称查询用户
        LoginUser loginUser = userMapper.findByUsername(userName);
        if (loginUser == null) {
            throw new BadCredentialsException("USER_IS_NOT_EXIST");
        }
        //通过用户查询权限
        List<Permission> permissionByUsername = userMapper.findPermissionByUsername(userName);
        Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
        for (Permission permission : permissionByUsername) {
            grantedAuthorities.add(new SimpleGrantedAuthority(permission.getCode()));
        }
        return new User(loginUser.getUsername(),loginUser.getPassword(),grantedAuthorities);
    }
}

3)创建config类

/**
 * Security配置类
 */
@EnableWebSecurity
//开启全局的细粒度方法级别权限控制功能 prePostEnabled=true支持@PreAuthorize("hasRole('ROLE_UC_REQUIREMENTS_SELECT')注解
//securedEnabled = true 支持@Secured({"ROLE_SELLERS_EDIT","ROLE_BUYERS_EDIT"})
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsService userDetailsService;//用户详情查询服务组件的接口
    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 认证
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);

    }
    /**
     * 授权
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //设置匹配的资源放行
        http.authorizeRequests().antMatchers("/","/asserts/**","/pages/login.html","/userlogin")
                .permitAll()
                .anyRequest().authenticated();//剩余任何资源必须认证
        //自动开启登录页
        http.formLogin().loginPage("/userlogin");
        //开启自动配置的注销功能
        // 1、访问 /logout 表示用户注销,清空session
          //2、注销成功会返回 /login?logout 页面;
        http.logout().logoutSuccessUrl("/userlogin");//注销之后来到登录页
        //开启记住我功能
        http.rememberMe().rememberMeParameter("remeber");
        //登陆成功以后,将cookie发给浏览器保存,以后访问页面带上这个cookie,只要通过检查就可以免登录
        //点击注销会删除cookie
        //自定义异常处理
        http.exceptionHandling().accessDeniedPage("/error403");
        http.csrf().disable();
    }
}

4)运行测试结果,密码不一致,跳转到登录页,并提示错误消息

 4)debug测试登录-断点调试

 1.断点-方法栈(先进后出原则)

 2.自定义UserDetailService实现类

 3 Dao层认证提供者: DaoAuthenticationProvider,Dao层认证提供者DaoAuthenticationProvider,用于调用自定义的UserDetailService实现类方法

4 抽象层用户认证提供者: AbstractUserDetailsAuthenticationProvider

抽象层用户认证提供者,获取dao层查找的认证用户信息,被封装成UserDetails对象,User类是UserDetails接口实现类

 

1) org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks认证用户账号是否被锁定,

是否启用,是否过期;用户表中可以增加这些字段。

2) public interface Authentication extends Principal 封装表单提交的认证信息

 3)密码匹配,PasswordEncoder实现密码匹配

 5.总结:

页面表单提交

1.页面提交的账号密码被封装到了Authtication对象中UsernamePasswordAuthenticationToken
2.数据库查询的对象放在了UserDetail对象中User
3.用PasswordEncoder(密码解码器)讲Authtication里面页面传来的明文密码使用加密器加密,然后和数据库User里面的明文进行
Equals()判断

3.5.3基于数据库(MD5密码)认证

1.定义PasswordEncoder接口实现类

/**
 * 密码匹配器
 */
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {

    /**
     * 加密
     * @param charSequence
     * @return
            */
    @Override
    public String encode(CharSequence charSequence) {
        return MD5Util.encode((String) charSequence);
    }

    /**比较登录密码和数据库密码是否一致
     * 参数1:输入的密码
     * 参数2;数据库查询的密码
     * @param rawPassword
     * @param encodedPassword
     * @return
     */
    // 加密的密码与数据库密码进行比对
//CharSequence rawPassword 表单字段 encodedPassword
数据库加密字段 @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { // 返回true 表示认证成功 返回fasle 认证失败 return MD5Util.encode((String) rawPassword).equalsIgnoreCase(encodedPassword); } }

2.debug测试

1)表单提交密码:rawPassword

 2)数据库存储密码:encodePassword

 3)调用自定义的密码匹配器

4)密码不一致,抛异常:Bad credentials ;密码一致,通过认证

 5)创建UsernamePasswordAuthenticationToken 对象,封装认证信息

 3.6细粒度权限控制

3.6.1前置细节【RoleAuthority的区别】

1.用户权限的表示:

1)roles("ADMIN","学徒","宗师")

2)authorities("USER","MANAGER");

2.给资源授予权限(角色或权限)

//.antMatchers("/level1/**").hasRole("学徒")
//.antMatchers("/level1/**").hasAnyRole("学徒","ADMIN")//拥有任何一个角色都可以访问
.antMatchers("/level1/**").hasAnyAuthority("学徒","ADMIN") //拥有任何一个权限都可以访问
.antMatchers("/level2/**").hasRole("大师")
.antMatchers("/level3/**").hasRole("宗师")

3.权限:【roles和authorities区别】

1) roles("ADMIN","学徒","宗师")

增加"ROLE_"前缀存放:【"ROLE_ADMIN","ROLE_学徒","ROLE_宗师"】

表示拥有的权限。一个角色表示的是多个权限

用户传入的角色不能以ROLE_开头,否则会报错。ROLE_是自动加上的

如果我们保存的用户的角色:直接传入角色的名字,权限【new SimpleGrantedAuthority("ROLE_" + role)】保存即可

2)authorities("USER","MANAGER");

原样存放:【"USER","MANAGER"】

表示拥有的权限。

如果我们保存的是真正的权限;直接传入权限名字,权限【new SimpleGrantedAuthority(role)】保存

无论是Role还是Authority都保存在  List<GrantedAuthority>,每个用户都拥有自己的权限集合->List<GrantedAuthority>

4.验证用户权限

1) 通过角色(权限)验证:

.antMatchers("/level1/**").hasRole("学徒")

.antMatchers("/level1/**").hasAnyRole("学徒","ADMIN")

拥有任何一个角色都可以访问

验证时会自动增加"ROLE_"进行查找验证:【"ROLE_学徒","ROLE_ADMIN"】

通过权限验证

.antMatchers("/level1/**").hasAuthority("学徒")

.antMatchers("/level1/**").hasAnyAuthority("学徒","ADMIN")

拥有任何一个权限都可以访问

验证时原样查找进行验证:【"学徒","ADMIN"】

3.6.2细粒度的资源控制

1.authenticated():通过认证的用户都可以访问

2.permitAll():允许所有人访问,即使未登录

3. authorizeRequests():更细粒度的控制

4.access("hasRole('大师') AND hasAuthority('user:delete') OR hasIpAddress('192.168.50.15')") 

 3.6.3细粒度的资源控制相应注解

1.开启注解控制权限模式

@EnableWebSecurity开启 Spring Security 注解

@EnableGlobalMethodSecurity(prePostEnabled=true):开启全局的细粒度方法级别权限控制功能,prePostEnabled=true

开启@PreAuthorize()注解,securedEnabled = true 支持@Secured

2.几个权限注解

1)@PreAuthorize:方法执行前检查

@PreAuthorize("hasRole('ADMIN')")  
public void addUser(User user){  
    //如果具有ROLE_ADMIN 权限 则访问该方法  
    ....  
}

2)PostAuthorize:方法执行后检查

@PostAuthorize:允许方法调用,但是,如果表达式结果为false抛出异常  
//returnObject可以获取返回对象user,判断user属性username是否和访问该方法的用户对象的用户名一样。不一样则抛出异常。  
@PostAuthorize("returnObject.user.username==principal.username")  
public User getUser(int userId){  
   //允许进入
...  
    return user;
}

3)@Seuried:拥有指定角色才可以访问方法

@Secured('ADMIN')   等价于    @PreAuthorize("hasRole('ADMIN')") 

4)细粒度的资源控制注解中可写的表达式

https://docs.spring.io/spring-security/site/docs/4.0.1.RELEASE/reference/htmlsingle/#el-common-built-in

所有表达式见文档

 3.6.5细粒度权限控制实现步骤

1)开启全局的细粒度方法级别权限控制功能

@EnableWebSecurity
//开启全局的细粒度方法级别权限控制功能 prePostEnabled=true支持@PreAuthorize("hasRole('ROLE_UC_REQUIREMENTS_SELECT')注解
//securedEnabled = true 支持@Secured({"ROLE_SELLERS_EDIT","ROLE_BUYERS_EDIT"})
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

2)给访问资源的方法增加注解,进行访问授权

@Controller
public class IndexController {

    /**
     * 授权(权限检查)使用AOP;MethodSecurityInterceptor
     * 方法执行之前AccessDecisionManager利用投票机制决定这个方法是否可运行
     */
    private final String PREFIX = "pages/";
    /**
     * 欢迎页
     * @return
     */
    @GetMapping("/")
    public String index() {
        return "welcome";
    }
    @GetMapping("/error403")
    public String error403() {
        return "error403";
    }

    /**
     * 登陆页
     * @return
     */
    @GetMapping("/userlogin")
    public String loginPage() {
        return PREFIX+"login";
    }


    /**
     * level1页面映射
     * @param
     * @return
     */
    @PreAuthorize("hasRole('学徒') AND hasAuthority('quanzhen')")
    @GetMapping("/level1/1")
    public String level1() {
        return PREFIX+"level1/"+1;
    }

    /**
     * level2页面映射
     * @param
     * @return
     */
    @Secured({"ROLE_SELLERS_EDIT","ROLE_BUYERS_EDIT"})
   /* @PreAuthorize("hasRole('ROLE_UC_REQUIREMENTS_SELECT')")*/
    @GetMapping("/level1/2")
    public String level2() {
        return PREFIX+"level1/"+2;
    }

    /**
     * level3页面映射
     * @param
     * @return
     */
    @GetMapping("/level1/3")
    public String level3() {
        return PREFIX+"level1/"+3;
    }
}

3)通过数据库加载用户权限

@Component
public class DefaultUserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
                if(StringUtils.isEmpty(userName)){
                    throw new RuntimeException("用户名不能为空");
        }
        //通过用户名称查询用户
        LoginUser loginUser = userMapper.findByUsername(userName);
        if (loginUser == null) {
            throw new BadCredentialsException("USER_IS_NOT_EXIST");
        }
        //用户拥有的权限=[角色+权限]
        Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
        //通过用户查询角色
        List<Role> roles = userMapper.finRoleByUserName(userName);
        for (Role role : roles) {
            grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_"+role.getRoleName()));
        }
        //通过用户查询权限
        List<Permission> permissionByUsername = userMapper.findPermissionByUsername(userName);
        for (Permission permission : permissionByUsername) {
            grantedAuthorities.add(new SimpleGrantedAuthority(permission.getCode()));
        }
        return new User(loginUser.getUsername(),loginUser.getPassword(),grantedAuthorities);
    }
}

4)mapper接口

@Mapper
public interface UserMapper {
    // 查询用户信息
    @Select(" select * from sys_user where username = #{userName}")
    LoginUser findByUsername(@Param("userName") String userName);

    // 查询用户的权限
    @Select(" select permission.* from sys_user user" + " inner join sys_user_role user_role"
            + " on user.id = user_role.user_id inner join "
            + "sys_role_permission role_permission on user_role.role_id = role_permission.role_id "
            + " inner join sys_permission permission on role_permission.perm_id = permission.id where 
user.username = #{userName};") List<Permission> findPermissionByUsername(@Param("userName") String userName); @Select("SELECT a.* FROM sys_role a , sys_user_role b ,sys_user c" + " WHERE a.id = b.role_id AND c.id = b.user_id WHERE c.username = #{userName}") List<Role> finRoleByUserName(@Param("userName") String userName); }

5)测试访问结果

登录认证通过,可以登录到成功页面,授权的路径拥有角色和权限的则可以访问,否则则出现如图所示的结果:

 没有授权的路径则可以继续访问,不影响,授权的路径则按照授权规则进行验证

 3.6.6、SpringSecurity-原理

 1.认证原理-过滤器链的调用

1)源码调试分析

方法栈:先进后出

  • 程序入口
  • 打断点-第一批次(一般在起点和终点打上一个断点)
  • 运行调试
  • 打断点-关键点

 2)过滤器

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

 3).debug开始

3.1过滤器:功能扩展的多个过滤器->责任链设计模式

 

 3.2获取过滤器链中的过滤器,封装为虚拟的VirtualFilterChain对象,并开始执行过滤

 3.3开始一个一个的执行过滤器

 2.认证原理UsernamePasswordAuthenticationFilter

2.1)UsernamePasswordAuthencationFilter源码流程-不带图

1、获取到页面的用户名和密码

2、将username和password包装成UsernamePasswordAuthenticationToken

3、获取系统的认证管理器(AuthenticationManager)来调用authenticate方法完成认证

3.1)AuthenticationManager获取ProviderManager来调用ProviderManager.authenticate()

3.2)ProviderManager获取到所有的AuthenticationProvider判断当前的提供者能否支持,如果支持provider.authenticate(authentication);

现在我们DaoAuthenticationProvider( authentication :页面封装用户名和密码的对象)

3.2.1)retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);

3.2.1.1)loadedUser = this.getUserDetailsService().loadUserByUsername(username);

(调用我们自己的UserDetailsService来去数据库查用户,按照用户名查出来的用户的详细信息)封装成UserDetail

3.2.2)preAuthenticationChecks.check(user);(预检查,账号是否被锁定等…)

3.2.3)additionalAuthenticationChecks(附加的认证检查)

3.2.3.1)使用passwordEncoder. matches检查页面的密码和数据库的密码是否一致

3.2.4)postAuthenticationChecks.check(user);(后置认证,检查密码是否过期)

3.2.5)createSuccessAuthentication:将认证成功信息重新封装成UsernamePasswordAuthenticationToken

3.3)3.2又返回了一个新的UsernamePasswordAuthenticationToken,然后擦掉密码

4、 eventPublisher.publishAuthenticationSuccess(result);告诉所有监听器认证成功了

2.2)UsernamePasswordAuthencationFilter源码流程-带图

1.执行过滤器,获取到页面的用户名和密码

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter 
 

 2)将username和password包装成UsernamePasswordAuthenticationToken

3)获取系统的认证管理器(AuthenticationManager)来调用authenticate方法完成认证(this.getAuthenticationManager().authenticate(authRequest))

 AuthenticationManager获取ProviderManager来调用ProviderManager.authenticate() 

4)ProviderManager获取到所有的AuthenticationProvider判断当前的提供者能否支持,如果支持provider.authenticate(authentication);

DaoAuthenticationProvider( authentication :页面封装用户名和密码的对象)(此图来源于网络)

4.1)、retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);

4.2)、 loadedUser = this.getUserDetailsService().loadUserByUsername(username);

(调用我们自己的UserDetailsService来去数据库查用户,按照用户名查出来的用户的详细信息)封装成UserDetail

4.3)、 preAuthenticationChecks.check(user);(预检查,账号是否被锁定等…)

4.4)、 additionalAuthenticationChecks(附加的认证检查)

4.5)、使用passwordEncoder. matches检查页面的密码和数据库的密码是否一致

4.6)、 postAuthenticationChecks.check(user);(后置认证,检查密码是否过期)

4.7)、 createSuccessAuthentication:将认证成功的信息重新封装成UsernamePasswordAuthenticationToken

4.8)、 3.2又返回了一个新的UsernamePasswordAuthenticationToken,然后擦掉密码

5.eventPublisher.publishAuthenticationSuccess(result);认证成功

3.认证原理-流程及相关类(API)

3.1认证流程

  1. 用户使用用户名和密码登录
  2. 用户名密码被过滤器(默认为 UsernamePasswordAuthenticationFilter)获取到,封装成 Authentication(UsernamePasswordAuthenticationToken)
  3. token(Authentication实现类)传递给 AuthenticationManager 进行认证
  4. AuthenticationManager 认证成功后返回一个封装了用户权限信息的 Authentication 对象
  5. 通过调用 SecurityContextHolder.getContext().setAuthentication(...)  将 Authentication 对象赋给当前的 SecurityContext 
  6. 将用户的信息保存到当前线程上,共享起来
  7. SecurityContextHolder.getContext();就能获取到之前认证好的Authentication对象(UsernamePasswordAuthenticationToken) 

3.2默认执行顺序

1.UsernamePasswordAuthenticationFilter 

1)用户通过url:/login 登录,该过滤器接收表单用户名密码
2)判断用户名密码是否为空
3)生成 UsernamePasswordAuthenticationToken
4)将 Authentiction 传给 AuthenticationManager接口的 authenticate 方法进行认证处理
5)AuthenticationManager 默认是实现类为 ProviderManager ,ProviderManager 委托给 AuthenticationProvider 进行处理
6)UsernamePasswordAuthenticationFilter 继承了 AbstractAuthenticationProcessingFilter 抽象类,
AbstractAuthenticationProcessingFilter 
在 successfulAuthentication 方法中对登录成功进行了处理,通过 SecurityContextHolder.getContext().setAuthentication() 
方法将 Authentication 认证信息对象绑定到 SecurityContext
7)下次请求时,在过滤器链头的 SecurityContextPersistenceFilter 会从 Session 中取出用户信息并生成 
Authentication(默认为 UsernamePasswordAuthenticationToken),
并通过 SecurityContextHolder.getContext().setAuthentication() 方法将 Authentication 认证信息对象绑定到 SecurityContext
需要权限才能访问的请求会从 SecurityContext 中获取用户的权限进行验证 

2.DaoAuthenticationProvider(实现了 AuthenticationProvider)

通过 UserDetailsService 获取 UserDetails

将 UserDetails 和 UsernamePasswordAuthentionToken 进行认证匹配用户名密码是否正确

若正确则通过 UserDetails 中用户的权限、用户名等信息生成新的 Authentication 认证对象并返回

3.2相关类

1. WebSecurityConfigurerAdapter

为创建 WebSecurityConfigurer 实例提供方便的基类,重写它的 configure 方法来设置安全细节

configure(HttpSecurity http):重写该方法,通过 http 对象的 authorizeRequests()方法定义URL访问权限,默认会为 formLogin() 提供一个简单的测试HTML页面

configure (AuthenticationManagerBuilder auth):通过 auth 对象的方法添加身份验证

 2.SecurityContextHolder

SecurityContextHolder 用于存储安全上下文信息(如操作用户是谁、用户是否被认证、用户权限有哪些),它用 ThreadLocal 来保存 SecurityContext,

者意味着 Spring Security 在用户登录时自动绑定到当前现场,用户退出时,自动清除当前线程认证信息,SecurityContext 中含有正在访问系统用户的详细信息

3.UserDetailsService

该接口仅有一个方法 loadUserByUsername,Spring Security 通过该方法获取用户信息

4.UserDetails

代表了Spring Security的用户实体类,带有用户名、密码、权限特性等性质,可以自己实现该接口,供 Spring Security 安全认证使用,Spring Security 默认使用的是内置的 User 类将从数据库获取的 User 对象传入实现该接口的类,并获取 User 对象的值来让类实现该接口的方法

通过 Authentication.getPrincipal() 的返回类型是 Object,但很多情况下其返回的其实是一个 UserDetails 的实例 

5.Authentication

Authentication 是一个接口,用来表示用户认证信息,在用户登录认证之前相关信息(用户传过来的用户名密码)会封装为一个 Authentication 具体实现类对象,默认情况下为 UsernamePasswordAuthenticationToken,登录之后(通过AuthenticationManager认证)会生成一个更详细的、包含权限的对象,然后把它保存在权限线程本地的 SecurityContext 中,供后续权限鉴定用Authentication.principal可以获取到已经认证的用户详细信息

 UsernamePasswordAuthenticationToken (密码被擦除,authenticated=true)

6.GrantedAuthority

GrantedAuthority 是一个接口,它定义了一个 getAuthorities() 方法返回当前 Authentication 对象的拥有权限字符串,用户有权限是一个 GrantedAuthority 数组,每一个 GrantedAuthority 对象代表一种用户权限

7.AuthenticationManager

AuthenticationManager 是用来处理认证请求的接口,它只有一个方法 authenticate(),该方法接收一个 Authentication 作为对象,如果认证成功则返回一个封装了当前用户权限信息的 Authentication 对象进行返回它默认的实现是 ProviderManager,但它不处理认证请求,而是将委托给 AuthenticationProvider 列表,然后依次使用 AuthenticationProvider 进行认证,如果有一个 AuthenticationProvider 认证的结果不为null,则表示成功(否则失败,抛出 ProviderNotFoundException),之后不在进行其它 AuthenticationProvider 认证,并作为结果保存在 ProviderManager认证校验时最常用的方式就是通过用户名加载 UserDetails,然后比较 UserDetails 密码与请求认证是否一致,一致则通过,Security 内部的 DaoAuthenticationProvider 就是使用这种方式认证成功后加载 UserDetails 来封装要返回的 Authentication 对象,加载的 UserDetails 对象是包含用户权限等信息的。认证成功返回的 Authentication 对象将会保存在当前的 SecurityContext 中 

8.AuthenticationProvider

AuthenticationProvider 是一个身份认证接口,实现该接口来定制自己的认证方式,可通过 UserDetailsSevice 对获取数据库中的数据

该接口中有两个需要实现的方法:

Authentication authenticate(Authentication authentication):认证处理,返回一个 Authentication 的实现类则代表成功,返回 null 则为认证失败

supports(Class<?> aClass):如果该 AuthenticationProvider 支持传入的 Authentication 认证对象,则返回 true ,如:return aClass.equals(UsernamePasswordAuthenticationToken.class); 

9.HttpSecurity

用于配置全局 Http 请求的权限控制规则,对哪些请求进行验证、不验证等

常用方法:

authorizeRequests():返回一个配置对象用于配置请求的访问限制

formLogin():返回表单配置对象,当什么都不指定时会提供一个默认的,如配置登录请求,还有登录成功页面

logout():返回登出配置对象,可通过logoutUrl设置退出url

antMatchers:匹配请求路径或请求动作类型,如:.antMatchers("/admin/**")

addFilterBefore: 在某过滤器之前添加 filter

addFilterAfter:在某过滤器之后添加 filter

addFilterAt:在某过滤器相同位置添加 filter,不会覆盖相同位置的 filter

hasRole:结合 antMatchers 一起使用,设置请求允许访问的角色权限或IP,如:

antMatchers("/admin/**").hasAnyRole("ROLE_ADMIN","ROLE_USER")

方法名        

用途

access(String)        

SpringEL表达式结果为true时可访问

anonymous()        

匿名可访问

denyAll()        

用户不可以访问

fullyAuthenticated()        

用户完全认证访问(非remember me下自动登录)

hasAnyAuthority(String…)        

参数中任意权限可访问

hasAnyRole(String…)        

参数中任意角色可访问

hasAuthority(String)        

某一权限的用户可访问

hasRole(String)        

某一角色的用户可访问

permitAll()        

所有用户可访问

rememberMe()        

允许通过remember me登录的用户访问

authenticated()        

用户登录后可访问

hasIpAddress(String)        

用户来自参数中的IP可访问

 

10.PasswordEncoder

Spring 提供的一个用于对密码加密的接口,首选实现类为 BCryptPasswordEncoder 

11.BCryptPasswordEncoder

spring security中的BCryptPasswordEncoder方法采用SHA-256 +随机盐+密钥  对密码进行加密。SHA系列是Hash算法,不是加密算法,使用加密算法意味着可以解密(这个与编码/解码一样),但是采用Hash处理,其过程是不可逆的。

(1)加密(encode):注册用户时,使用SHA-256+随机盐+密钥把用户输入的密码进行hash处理,得到密码的hash值,然后将其存入数据库中。

(2)密码匹配(matches):用户登录时,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是使用相同的算法把用户输入的密码进行hash处理,得到密码的hash值,然后将其与从数据库中查询到的密码hash值进行比较。如果两者相同,说明用户输入的密码正确。

这正是为什么处理密码时要用hash算法,而不用加密算法。因为这样处理即使数据库泄漏,黑客也很难破解密码




 
 

 

猜你喜欢

转载自www.cnblogs.com/cxyyh/p/11832687.html