从源码看Spring Security之采坑笔记(Spring Boot篇)

简介: 从源码看Spring Security之采坑笔记(Spring Boot篇)

一、唠嗑


鼓捣了两天的Spring Security,踩了不少坑。如果你在学Spring Security,恰好又是使用的Spring Boot,那么给我点个赞吧!这篇博客将会让你了解Spring Security的各种坑!

二、开始


1、准备


  • Spring boot 1.5
  • Mysql 5.7
  • 导入依赖


<!-- Web工程 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 数据库相关 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- security 核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- thymeleaf 模板-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- 可以在HTML使用sec标签操作Security -->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity4</artifactId>
        </dependency>


2、开启Security并配置

package cn.zyzpp.security.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
 * Create by yster@foxmail.com 2018/6/10/010 18:07
 */
@EnableWebSecurity
public class MySerurityConfig extends WebSecurityConfigurerAdapter {
    /*自己实现下面两个接口*/
    @Autowired
    private AuthenticationProvider authenticationProvider;
    @Autowired
    private UserDetailsService userDetailsService;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/", "/signIn").permitAll()//所有人都可以访问
                .antMatchers("/leve/1").hasRole("VIP1") //设置访问角色
                .antMatchers("/leve/2").hasRole("VIP2")
                .antMatchers("/leve/3").hasAuthority("VIP2")//设置访问权限
                .anyRequest().authenticated() //其他所有资源都需要认证,登陆后访问
                .and()
                .formLogin()//开启自动配置的授权功能
                .loginPage("/login")    //自定义登录页(controller层需要声明)
                .usernameParameter("username")  //自定义用户名name值
                .passwordParameter("password")  //自定义密码name值
                .failureUrl("/login?error") //登录失败则重定向到此URl
                .permitAll() //登录页都可以访问
                .and()
                .logout()//开启自动配置的注销功能
                .logoutSuccessUrl("/")//注销成功后返回到页面并清空Session
                .and()
                .rememberMe()
                .rememberMeParameter("remember")//自定义rememberMe的name值,默认remember-Me
                .tokenValiditySeconds(604800);//记住我的时间/秒
    }
    /*定义认证规则*/
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    /*  保存用户信息到内存中
        auth.inMemoryAuthentication()
             .withUser("张三").password("123456").roles("VIP1")
             .and()
             .withUser("李四").password("123456").roles("VIP2");
    */
        /*自定义认证*/
        auth.authenticationProvider(authenticationProvider);
        auth.userDetailsService(userDetailsService);//不定义的话rememberMe报错
    }
    /*忽略静态资源*/
    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/resources/static/**");
    }
}

讲一下:


我们基本不会把用户信息保存在内存中,所以我们自定义认证方法。这里我推荐阅读 认证(Authentication)与源码解读。


自定义认证也有两种方法,第一是注入

DaoAuthenticationProvider(org.springframework.security.authentication.dao)

@Bean
    public DaoAuthenticationProvider daoAuthenticationProvider(){
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);//获取用户信息
        daoAuthenticationProvider.setPasswordEncoder(new Md5PasswordEncoder());//MD5加密
        daoAuthenticationProvider.setSaltSource(new SaltSource() {  //加盐
            @Override
            public Object getSalt(UserDetails user) {
                return user.getUsername();
            }
        });
        return daoAuthenticationProvider;
    }


然后改一下设置

auth.authenticationProvider(authenticationProvider);


这种方法我并不推荐,因为我们把密码错误的异常交给了Security底层去抛出,然而抛出的消息只是Bad credentials 这样的消息提示你会需要?


所以我们使用第二种方法,如下:


3、自定义AuthenticationProvider接口实现类


package cn.zyzpp.security.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.encoding.Md5PasswordEncoder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
/**
 * Create by yster@foxmail.com 2018/6/21/021 15:53
 * Authentication 是一个接口,用来表示用户认证信息的
 */
@Component
public class MyAuthenticationProvider implements AuthenticationProvider{
    @Autowired
    private MyUserDetailsService userDetailsService;
    @Override
    public Authentication authenticate(Authentication authentication){
        //1.获取用户输入的用户名 密码
        String username = authentication.getName();
        String password = (String) authentication.getCredentials();
        //2.关于MD5加密:
        //因为我们是自定义Authentication,所以必须手动加密加盐而不需要再配置。
        password = new Md5PasswordEncoder().encodePassword(password,username);
        //3.由输入的用户名查找该用户信息,内部抛出异常
        UserDetails user = userDetailsService.loadUserByUsername(username);
        //4.密码校验
        if (!password.equals(user.getPassword())) {
            throw new DisabledException("---->UserName :" + username + " password error!");
        }
        return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
    }
    @Override
    public boolean supports(Class<?> aClass) {
        return (UsernamePasswordAuthenticationToken.class
                .isAssignableFrom(aClass));
    }
}

讲一下:


这里说Security的一个坑:


相信你也看到了有的教程上说抛出UsernameNotFoundException 用户找不到,BadCredentialsException 坏的凭据,但这两个类都是继承自AuthenticationException抽象类,当你抛出这俩异常时,Security底层会捕捉到你抛出的异常,如图:


image.png


看到了吧,AuthenticationException异常并不会被抛出,debug调式一下,你就会感受到它的曲折历程,相当感人!然后莫名其妙的被换掉了,而且无解。

没错,你没看错,AccountStatusException异常被直接抛出了,这正是我们需要的;有的同学可能想到了自定义异常,但我们是结合Security框架,要按人家的规则来,不信你试试。

附一些常用异常:


image.png


4、自定义UserDetailsService接口实现类

package cn.zyzpp.security.config;
import cn.zyzpp.security.entity.Role;
import cn.zyzpp.security.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.GrantedAuthority;
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.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
 * 进行认证的时候需要一个 UserDetailsService 来获取用户的信息 UserDetails,
 * 其中包括用户名、密码和所拥有的权限等。
 * Create by yster@foxmail.com 2018/6/21/021 15:56
 */
@Component
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private UserService userService;
    /*
     * 采坑笔记:
     * new SimpleGrantedAuthority("...")时
     * 加前戳是Role,通过hasRole()获取,用来认证角色;
     * 不加前戳是Authoritiy,通过hasAuthority()获取,用来鉴定权限;
     * 总结:加前戳是角色,不加前戳是权限。此前戳只用于本类。
     */
    String role_ = "ROLE_";
    @Override
    public UserDetails loadUserByUsername(String username) {
        //1.业务层根据username获取该用户
        cn.zyzpp.security.entity.User user = userService.findUserByUserName(username);
        if (user == null) {
            //这里我们不抛出UsernameNotFoundException因为Security会把我们抛出的该异常捕捉并换掉;
            //这里要明确Security抛出的异常无法被ControllerAdvice捕捉到,无法进行统一异常处理;
            //而我们只需要打印正确的异常消息即可,Security自动把异常添加到HttpServletRequest或HttpSession中
            throw new DisabledException("---->UserName :" + username + " not found!");
        }
        //2.从业务层获取用户权限并转为Authorities
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : user.getRoleList()) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));//设置权限
            authorities.add(new SimpleGrantedAuthority(role_ + role.getName()));//设置角色
        }
        //3.返回Spring定义的User对象
        return new User(username, user.getPassword(), authorities);
    }
}

讲一下:


我们在保存用户信息到内存中时是这样的

auth.inMemoryAuthentication()
      .withUser("张三")
      .password("123456")
      .roles("ROLE_VIP1")
      .authorities("VIP1")


角色和权限是分开设置的,但我们在自定义时只有权限设置

authorities.add(new SimpleGrantedAuthority("权限名"));


定义以后你会发现这真真真…的是权限,不是角色,联想到上面Security的角色和权限其

实是不同的,我想我应该是错过了什么?


然后翻看Security源码:


image.png


翻译过来:如果调用hasRole(“ADMIN”)或hasRole(“ROLE_ADMIN”)方法时,当Role前缀为”ROLE_”(默认)时将使用ROLE_ADMIN角色。


而我们在把用户信息保存到内存时,底层是这样的:


image.png


解读一下就是在调用.roles("ROLE_VIP1")方法注册Role时,先通过role.startsWith("ROLE_")断言输入的角色名是否是"ROLE_"开头的,如果不是,补充"RELE_"前戳。


所以,Security解决角色和权限分开的依据就是是否含有"ROLE_"前戳,该默认前戳也是可以自己修改的。


ok,继续我们的Security学习之路。


5、获取Security登录异常信息

package cn.zyzpp.security.controller;
...
import cn.zyzpp.security.service.UserService;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
 * Create by yster@foxmail.com 2018/6/10/010 18:35
 */
@Controller
public class MyController {
    @Autowired
    UserService userService;
    @Autowired
    HttpSession session;
    @Autowired
    HttpServletRequest request;
    /*ModelMap的Key*/
    final String ERROR = "error";
    /**
     * 自定义登录页并进行异常信息提示
     * 需要在Security中设置
     */
    @RequestMapping(value = "/login")
    public String login(ModelMap modelMap){
      /*
       security的AuthenticationException异常自动保存在request或session中
       官方默认保存在Session,但我们自定义过多。我测试是在request中。
       所以在html页面还需要搭配th:if="${param.error!=null}"检查Url是否有参数error
        */
        String key = WebAttributes.AUTHENTICATION_EXCEPTION;
        if (session.getAttribute(key)!=null){
//            System.out.println("request");
            AuthenticationException exception = (AuthenticationException) session.getAttribute(key);
            modelMap.addAttribute(ERROR,exception.getMessage());
        }
        if (request.getAttribute(key)!=null){
//            System.out.println("session");
            AuthenticationException exception = (AuthenticationException) request.getAttribute(key);
            modelMap.addAttribute(ERROR,exception.getMessage());
        }
        return "login";
    }
}

自定义login登录页面


Security规定若是GET访问则是请求页面,POST访问则为提交登录

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>登录页面</title>
</head>
<body>
<form th:action="@{/login}" method="post">
    用户名:<input type="text" placeholder="username" name="username" required=""/><br/>
    密码:<input type="password" placeholder="password" name="password" required=""/><br/>
    记住我:<input type="checkbox" name="remember"/>
    <input type="submit" value="提交"/>
    <span th:if="${param.error!=null}" th:text="${error}"/>
</form>
</body>
</html>

讲一下:


如果你debug追踪一下,你就可以了解Security的运行原理

Security的SimpleUrlAuthenticationFailureHandler(简单认证故障处理)会把异常保存到request或session中,forwardToDestination默认为false,也就是保存在session,实际我们测试是保存在request。


image.png


6、在view层使用Security


6.1 使用HTML sec标签 (推荐)


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
    <meta charset="UTF-8"/>
    <title>首页</title>
</head>
<body>
<div sec:authorize="isAuthenticated()">
    <form th:action="@{/logout}" method="POST">
        <input type="submit" value="注销" />
    </form>
    user:<b sec:authentication="name"></b><br/>
    <!-- principal对应org.springframework.security.core.userdetails.User类 -->
    Role:<b sec:authentication="principal.authorities"></b>
</div>
<div sec:authorize="!isAuthenticated()">
    <h2>游客你好!</h2>请<a th:href="@{/login}">登录</a>
</div>
<div sec:authorize="hasRole('VIP1')">
    <h2>ROLE_VIP1_可见</h2>
</div>
<div sec:authorize="hasRole('VIP2')">
    <h2>ROLE_VIP2_可见</h2>
</div>
<div sec:authorize="hasAuthority('VIP1')">
    <h2>Authority:VIP1_可见</h2>
</div>
</body>
</html>

6.2 编码获取用户登录信息


下面为我自己写的方法,看看就好!

/**
     * 不使用sec标签(不推荐)
     * 在Controller获取用户信息
     */
    @RequestMapping("/index")
    public String index1(ModelMap model){
        userAndRoles(model);
        return "index";
    }
    /**
     * Security辅助方法:获取用户信息
     */
    private void userAndRoles(ModelMap model) {
        //从Security获取当前用户会话
        Object principal = SecurityContextHolder.getContext()
                .getAuthentication()
                .getPrincipal();
        User user = null;
        //判断用户已经登录
        if (principal instanceof User){
            user = (User) principal;
            //遍历迭代器获取用户权限
            Iterator<GrantedAuthority> iterator = user.getAuthorities().iterator();
            List<String> roles = new ArrayList<>();
            while (iterator.hasNext()){
                roles.add(iterator.next().getAuthority());
            }
            //保存角色信息
            model.addAttribute("roles",roles.toString());
        }
        //保存用户信息,未登录为空
        model.addAttribute("user",user);
    }


7、权限及用户的Entity类


权限表

/**
 * 权限表
 * Create by yster@foxmail.com 2018/6/21/021 18:00
 */
@Entity
@Table(name = "role")
public class Role {
    @Id
    @GeneratedValue
    private int id;
    private String name;
    ...
}


用户表

/**
 * Create by yster@foxmail.com 2018/6/21/021 17:59
 */
@Entity
@Table(name = "user",uniqueConstraints = {@UniqueConstraint(columnNames="username")})
public class User {
    @Id
    @GeneratedValue
    private int id;
    private String username;
    private String password;
    @OneToMany(cascade={CascadeType.ALL}, fetch=FetchType.EAGER)
    @JoinColumn(name = "r_id")
    private List<Role> roleList;
    ....
}


目录
相关文章
|
23天前
|
JSON 安全 Java
什么是JWT?如何使用Spring Boot Security实现它?
什么是JWT?如何使用Spring Boot Security实现它?
84 5
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
76 2
|
1月前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
|
10天前
|
存储 JavaScript 前端开发
基于 SpringBoot 和 Vue 开发校园点餐订餐外卖跑腿Java源码
一个非常实用的校园外卖系统,基于 SpringBoot 和 Vue 的开发。这一系统源于黑马的外卖案例项目 经过站长的进一步改进和优化,提供了更丰富的功能和更高的可用性。 这个项目的架构设计非常有趣。虽然它采用了SpringBoot和Vue的组合,但并不是一个完全分离的项目。 前端视图通过JS的方式引入了Vue和Element UI,既能利用Vue的快速开发优势,
64 13
|
26天前
|
监控 Java 数据库连接
详解Spring Batch:在Spring Boot中实现高效批处理
详解Spring Batch:在Spring Boot中实现高效批处理
138 12
|
18天前
|
JavaScript 安全 Java
java版药品不良反应智能监测系统源码,采用SpringBoot、Vue、MySQL技术开发
基于B/S架构,采用Java、SpringBoot、Vue、MySQL等技术自主研发的ADR智能监测系统,适用于三甲医院,支持二次开发。该系统能自动监测全院患者药物不良反应,通过移动端和PC端实时反馈,提升用药安全。系统涵盖规则管理、监测报告、系统管理三大模块,确保精准、高效地处理ADR事件。
|
20天前
|
存储 缓存 Java
Spring面试必问:手写Spring IoC 循环依赖底层源码剖析
在Spring框架中,IoC(Inversion of Control,控制反转)是一个核心概念,它允许容器管理对象的生命周期和依赖关系。然而,在实际应用中,我们可能会遇到对象间的循环依赖问题。本文将深入探讨Spring如何解决IoC中的循环依赖问题,并通过手写源码的方式,让你对其底层原理有一个全新的认识。
40 2
|
26天前
|
安全 Java 测试技术
详解Spring Profiles:在Spring Boot中实现环境配置管理
详解Spring Profiles:在Spring Boot中实现环境配置管理
73 10
|
23天前
|
负载均衡 Java 开发者
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
72 5
|
1月前
|
前端开发 Java 开发者
Spring生态学习路径与源码深度探讨
【11月更文挑战第13天】Spring框架作为Java企业级开发中的核心框架,其丰富的生态系统和强大的功能吸引了无数开发者的关注。学习Spring生态不仅仅是掌握Spring Framework本身,更需要深入理解其周边组件和工具,以及源码的底层实现逻辑。本文将从Spring生态的学习路径入手,详细探讨如何系统地学习Spring,并深入解析各个重点的底层实现逻辑。
64 9