目前技术栈:Java基础,JavaWeb部分,SSM,SpringBoot,前端(html+js部分+vue部分),基本上都只学了理论而未实践
学习目的:想在学习若依管理系统的同时,回顾SpringBoot等知识,并掌握SpringSecurity以及vue的部分知识。学习完成后,完成自己的第一个项目。
学习资源:
http://doc.ruoyi.vip/ruoyi-vue/document/hjbs.html#准备工作
https://blog.youkuaiyun.com/donglinjob/category_10352718.html
https://blog.youkuaiyun.com/zimou5581/category_8892656.html
强烈推荐:目前看到最全最完整写的最好的
https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI1NDY0MTkzNQ==&action=getalbum&album_id=1319828555819286528&scene=173&from_msgid=2247489405&from_itemidx=2&count=3&nolastread=1#wechat_redirect
补充:本文以实践为主,所以各个知识点的原理不会深入,可以参考学习资料中的原理部分。在每篇博文的最后,会有demo仿写,仿写代码放在github了:资源地址
Spring Security
学习建议:准备一个空的springboot项目和若依的项目,按照博客中的内容一边实践一边学习若依的代码。
Spring Security 是一个高度自定义的 安全框架。利用 Spring IoC/DI和 AOP 功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。
Spring Security是基于Spring的一个安全框架,最重要的功能是认证和授权,核心是 过滤器链 springSecurityFilterChain 【类型是FilterChainProxy】,过滤器链的每个元素都是一组URL对应一组过滤器,WebSecurity用来创建FilterChainProxy过滤器,HttpSecurity用来创建过滤器链的每个元素,具体的实现请看 4. SpringSecurity配置类。
设计模式使用到了 适配器模式 和 建造者模式。
博文推荐:
https://blog.youkuaiyun.com/zimou5581/article/details/102457672
https://blog.youkuaiyun.com/u012702547/article/details/107655180
深入源码级别的原理剖析,值得一看
SpringSecurity知识点概述
1. 依赖导入 + 测试
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
导入后,Spring Security 已经生效,默认拦截全部请求,如果用户没有登录,跳转到内置登录页面。账号是user,密码在idea的控制台中,是通过UUID随机生成的密码。如果密码输入错误,跳转到login?error:
注意,在 Spring Security 中,如果我们不做任何配置,默认的登录页面和登录接口的地址都是 /login,也就是说,默认会存在如下两个请求:
GET http://localhost:8080/login
POST http://localhost:8080/login
如果是 GET 请求表示你想访问登录页面,如果是 POST 请求,表示你想提交登录数据。
在 4.SecurityConfig 中,我们可以通过 loginProcessingUrl 方法来指定登录接口地址,如下:
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.permitAll()
.and()
这样配置之后,登录页面地址和登录接口地址就分开了,各是各的。
此时我们还需要修改登录页面里边的 action 属性,改为 /doLogin,如下:
<form action="/doLogin" method="post">
<!--省略-->
</form>
2. UserDetailsService接口 自定义账号密码
当没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以若我们要通过自定义逻辑控制认证逻辑,必须实现 UserDetailsService 接口以及拥有PasswordEncoder实例。 我们先来讲UserDetailsService。
2.1 接口定义:
2.2 返回值 UserDetails
2.3 UserDetails的实现类User
要想返回 UserDetails 的实例就只能返回接口的实现类。Spring Security 中提供了User这样一个实例。
我们重点看一下这个构造方法
username:用户名
password:密码
authorities:用户具有的权限。此处不允许为 null
此处的用户名应该是客户端传递过来的用户名。而密码应该是从数据库中查询出来的密码。Spring Security 会根据 User 中的 password和客户端传递过来的 password 进行比较。如果相同则表示认证通过,如果不相同表示认证失败。
authorities 里面的权限对于后面学习授权是很有必要的,包含的所有内容为此用户具有的权限,如有里面没有包含某个权限,而在做某个事情时必须包含某个权限则会出现 403。通常都是通过AuthorityUtils.commaSeparatedStringToAuthorityList(“”) 来 创 建authorities 集合对象的。参数是一个字符串,多个权限使用逗号分隔。
2.4 方法参数
方法参数是用户名,默认为username
2.5 异常
UsernameNotFoundException 用 户 名 没 有 发 现 异 常 。 在loadUserByUsername 中是需要通过自己的逻辑从数据库中取值的。如果 通 过 用 户 名 没 有 查 询 到 对 应 的 数 据 , 应 该 抛 出UsernameNotFoundException,系统就知道用户名没有查询到。
2.6 手撕若依源码
我们找到若依对于UserDetailsService的实现类UserDetailsServiceImpl
loadUserByUsername方法中,调用了userService去通过username获取user,然后调用createLoginUser方法,传入user和通过permissionService获取的用户权限,去返回一个userDetails。我们点开这个LoginUser,发现它是实现了UserDetails这个接口,也就是它是作者对UserDetails的一个扩展,本质上还是UserDetails。
感想:
UserDetailsServiceImpl中组合了两个Service,并调用autowired自动装配,使得后面可以通过调用这两个服务去获取所需要的信息,体现了IOC的思想。
3 PasswordEncoder 密码解析器
Spring Security 要求容器中必须有 PasswordEncoder 实例。所以当自定义登录逻辑时要求必须给容器注入 PaswordEncoder 的 bean 对象。作用就是给密码编码。
如果我们不使用数据库导入账号密码,可以在yml或者properties文件中配置:
spring.security.user.name=123
spring.security.user.password=123
或者在配置类中:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("javaboy.org")
.password("123").roles("admin");
}
3.1 接口介绍
可以看到,官方推荐的实例是BCryptPasswordEncoder,其他的解析器可以在idea中查看PasswordEncoder的实现类
encode():把参数按照特定的解析规则进行解析。
matches():验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
upgradeEncoding():如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回 false。默认返回 false。
3.2 BCryptPasswordEncoder
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。
BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10.
3.3 手撕若依源码
这地方很简单,就直接new一个实例就好了,然后在身份认证接口加密一下,身份认证接口请看4.3。
4. SpringSecurity配置类
4.1 @EnableGlobalMethodSecurity 注解
我们可以在配置类上加上这个注解来开启spring方法级别安全。
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
注解详解
prePostEnabled = true 会解锁 @PreAuthorize 和 @PostAuthorize 两个注解。从名字就可以看出 @PreAuthorize 注解会在方法执行前进行验证,而 @PostAuthorize 注解会在方法执行后进行验证。
打开若依里的一个Controller,我以SysConfigController为例子,我们可以看到这个类里用了多处 @PreAuthorize 注解,注解里面的是作者自定义的一个注解,封装了他自定义的安全检测。
补充:@EnableWebSecurity 是真正核心的安全注解,不过在导入spring-boot-start-security的时候已经自动注入了,所以不用担心。
4.2 WebSecurityConfigurerAdapter 抽象类
这个类是我们配置类必须要继承的抽象类,最重要的是以HttpSecurity或者AuthenticationManagerBuilder作为参数的两个configure,前者是对URL配置,后者是身份验证。
4.2.1 configure(HttpSecurity http) URL配置
httpSecurity的方法特别的多,我这里放一些比较常用的以及若依注释里面写的
4.2.1.1 登录成功重定向 URL
在 Spring Security 中,和登录成功重定向 URL 相关的方法有两个:
defaultSuccessUrl
successForwardUrl
这两个咋看没什么区别,实际上内藏乾坤。
首先我们在配置的时候,defaultSuccessUrl 和 successForwardUrl 只需要配置一个即可,具体配置哪个,则要看你的需求,两个的区别如下:
1.defaultSuccessUrl 有一个重载的方法,我们先说一个参数的 defaultSuccessUrl 方法。如果我们在 defaultSuccessUrl 中指定登录成功的跳转页面为 /index,此时分两种情况,如果你是直接在浏览器中输入的登录地址,登录成功后,就直接跳转到 /index,如果你是在浏览器中输入了其他地址,例如 http://localhost:8080/hello,结果因为没有登录,又重定向到登录页面,此时登录成功后,就不会来到 /index ,而是来到 /hello 页面。
2.defaultSuccessUrl 还有一个重载的方法,第二个参数如果不设置默认为 false,也就是我们上面的的情况,如果手动设置第二个参数为 true,则 defaultSuccessUrl 的效果和successForwardUrl 一致。
3.successForwardUrl 表示不管你是从哪里来的,登录后一律跳转到 successForwardUrl 指定的地址。例如 successForwardUrl 指定的地址为 /index ,你在浏览器地址栏输入 http://localhost:8080/hello,结果因为没有登录,重定向到登录页面,当你登录成功之后,就会服务端跳转到 /index 页面;或者你直接就在浏览器输入了登录页面地址,登录成功后也是来到 /index。
这让我想到了我们去看某些网站的文章时,比如csdn,有些文章要我们先登录才能看,它会跳转到登录页面,登录成功后,会跳回原页面,这可能就是使用了 defaultSuccessUrl;
而有些网站我们登录成功后,会跳转到首页,这可能就是使用了 successForwardUrl;
虽然允许两个都配置,但建议只配置一个,有兴趣的可以试试看两个都配置的时候,是哪个配置被覆盖掉了。
4.2.1.2 注销登录接口
注销登录的默认接口是 /logout,我们也可以配置。
.and()
.logout()
.logoutUrl("/logout")
.logoutRequestMatcher(new AntPathRequestMatcher("/logout","POST"))
.logoutSuccessUrl("/index")
.deleteCookies()
.clearAuthentication(true)
.invalidateHttpSession(true)
.permitAll()
.and()
注销登录的配置:
默认注销的 URL 是 /logout,是一个 GET 请求,我们可以通过 logoutUrl 方法来修改默认的注销 URL。
logoutRequestMatcher 方法不仅可以修改注销 URL,还可以修改请求方式,实际项目中,这个方法和 logoutUrl 任意设置一个即可。
logoutSuccessUrl 表示注销成功后要跳转的页面。
deleteCookies 用来清除 cookie。
clearAuthentication 和 invalidateHttpSession 分别表示清除认证信息和使 HttpSession 失效,默认可以不用配置,默认就会清除。
4.2.1.3 rememberMe
加了这个方法以后,就可以实现自动登录,cookie中多传了一个remember-me=on;
在浏览器关闭后,并重新打开之后,用户再去访问接口,此时会携带着 cookie 中的 remember-me 到服务端,服务到拿到值之后,可以方便的计算出用户名和过期时间,再根据用户名查询到用户密码,然后通过 MD5 散列函数计算出散列值,再将计算出的散列值和浏览器传递来的散列值进行对比,就能确认这个令牌是否有效。
4.2.2 configure(WebSecurity web)
一般是用来绕开静态资源访问的
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/js/**", "/css/**","/images/**");
}
这里要注意区分它和下面代码的区别
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatchers(