003-云E办_学习SpringSecurity
一、SrpingSecurity
1 、什么是安全框架
解决系统安全问题的框架。如果没有安全框架,我们需要手动处理每个资源的访问控制,非常 麻烦。使用安全框架,我们可以通过配置的方式实现对资源的访问限制。
2、Spring Security简介
Spring Security:Spring家族一员。是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决 方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制 反转Inversion of Control,DI:Dependency Injection 依赖注入)和 AOP(面向切面编程)功能,为应用系 统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作
Spring Security是一个高度自定义的安全框架。利用 Spring IoC/DI和AOP功能,为系统提供了声明式安全访问控 制功能,减少了为系统安全而编写大量重复代码的工作。
使用 Spring Secruity 的原因有很多,但大部分都是发现了 javaEE的 Servlet 规范或 EJB 规范中的安全功能缺乏典型企业应用场景。同时认识到他们在 WAR 或 EAR 级别无法移 植。因此如果你更换服务器环境,还有大量工作去重新配置你的应用程序。使用 Spring Security解决了这些问题, 也为你提供许多其他有用的、可定制的安全功能。
正如你可能知道的两个应用程序的两个主要区域是“认证”和“授 权”(或者访问控制)。这两点也是 Spring Security 重要核心功能。
“认证”,是建立一个他声明的主体的过程(一 个“主体”一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否能登录。
“授权”,指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否有权限去做某些事情。
3、创建demo
1.编写代码
当我们项目创建好时,选择了spring web/security时,发现xml文件以及导入了相应的依赖。
我们选择的是2.2.5版本的父部依赖。
2.创建controller
ResponseBody返回的是字符串,不能跳转
如果加了@ResponseBody这个注解,返回的就是字符串或是json对象,跳转不了
要注意的是控制成如果使用了@RestController 那么就是@ResponseBody和@Controller合在了一块。如果真的确定不需要用到转发,重定向之类的,那么就可以使用@RestController。否则还是不用偷懒
//@RestController <--> 包含ResponseBody
@Controller
public class LoginController {
/*
* 登录方法:
* */
@RequestMapping("/login")
public String login(){
System.out.println("执行了登录方法");
//重定向到主页:
return "redirect:main.html";
}
//在准备两个HTML:login.html / main.html
// 当浏览器访问“/login"时,给浏览器返回main.html
}
3.创建两个HTML文件:
在resource/static/ login–main.html
4.测试:
运行Application出现的界面如下:
-
那为什么会出现如下界面呢?我们自己写的界面呢?
其实这一句表明:spring security生效了。有了ssecurity以后,任何操作之前需要通过这个界面进行登录,然后才能做其他工作。 -
如何登录呢?
账号:默认的是user
密码:随机密码:8689f864-bc43-4ef9-b578-7f8ef8c1bfb8
在运行程序时,在控制台可以看到一条:Using generated security password: 8689f864-bc43-4ef9-b578-7f8ef8c1bfb8
后面这个随机数字就是密码了。
上面SpringSecurity界面,输入账号(user),密码(加密),登录成功以后,就是下面咱们写的界面:
ps:登录的次数只有一次。第二次就不会跳转成功了。也就是说明过期了。
二、 如何自定义逻辑(重要:源码分析)
上述小demo,当我们没有配置SpringSecurity时,账户和密码都是自动生成的。而实际当中账户和密码都应该是从数据库中查询出来的。所以我们应自定义逻辑去控制认证逻辑。
下述总体概括:userDetailsService接口 返回的是UserDetails接口,被user实现。
1、UserDetailsService分析?
idea中查询类的快捷键:ctrl+n
查找接口的实现类:ctrl+h
-
实现UserDetailsService接口:
只有一个方法:
UserDetails loadUserByUsername(String username);
//通过用户名加载用户(从浏览器传过来username)
-
那返回来的UserDetails是什么?
也是一个接口,实现了继承了序列号接口(Serializable)
拥有的方法:
getAuthorities():获取所有权限到这个用户。不能返回空。
getPassword():获取密码
getUsername():获取用户名
isAccountNonExpired():判断用户是否过期。
isAccontNonLocked():判断用户是否锁定
isCredentialsNonExpried():凭证是否过期。(密码是否过期)
isEnabled():是否可用 -
user 实现了UserDetails接口
是springSecurity定义的实现类。
拥有的构造函数:
有参构造:传入(username,password,autborities权限列表){
// username是从浏览器传过来的。去数据库查找用户,返回对应用户的相应信息(包括:密码、账户是否过期、锁定、凭证过期。)。比如拿着前端转过来的username,去数据库查找用户,拿到用户密码,再去前端输入的密码进行比较。
this.获取。判断上述方法。
}
下面图片中:username是前端用户传过来的,password是:拿着username去数据库找密码,找到后给password。这个password在和前端输入的密码进行比较。
2、passwordEncoder详解
passwordEncoder是接口。当我们需要自定义逻辑时,就需要实现该接口。
passwordEncoder接口的方法:
-
String encode(CharSequence rawPassword);
解释:加密,密码。好的加密方式,推荐用SHA-1算法,或者用hash算法。
rawPassword:就是原始的密码。进行相应的加密。返回字符串。 -
boolean matches(CharSequence rawPassword, String encodedPassword);
解释:匹配。rawPasswoed是原始密码,encodedPasswoed是加密的密码。进行判断是否一致。
官方推荐用的加密算法是:BCryptPasswordEncoder。
是基于hash算法,是单向的只能加密,不能解密。
测试passwoedEncoder:
@SpringBootTest
class SpringsecuritydemoApplicationTests {
@Test
void contextLoads() {
/**
* passwordEncoder是接口
* BCryptPasswordEncoder 是实现接口类
* encode方法是:传入一个密码值,然后进行加密,返回一个字符串
* mathes方法是:传入原始密码,传入加密后的面膜,返回是否相等
*
* 加密后的123是:
* $2a$10$dKjBIPKkdjOn6xiODHsPKe45/Rsfib53aW4SROZ44kNF745gV1znS
* 原始密码是否等于加密密码true
*/
PasswordEncoder pe = new BCryptPasswordEncoder();
String encode = pe.encode("123");
System.out.println("加密后的123是:"+encode);
boolean matches = pe.matches("123", encode);
System.out.println("原始密码是否等于加密密码"+matches);
}
}
三、自定义登录逻辑:
1、创建password实体类
1、但我们自定义登录逻辑时,容器内必须有Password实例,所以要写一个配置类。创建config/Securityconfig
/**
* 我们自定义登录逻辑时,容器内必须有Password实例,
* 所以要写一个配置类。创建config/Securityconfig
*
* 创建好了实例,那么实现UserDetailsService接口
*/
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder getPw(){
return new BCryptPasswordEncoder();
}
}
2、创建用户实现类
2、有了实例,就要实现userDetailsService接口。创建类:service/UserDetailsServiceImpl实现类。
/**
* pw是config/securityConfig下面的bean
* @Bean
* public PasswordEncoder getPw(){
* return new BCryptPasswordEncoder();
* }
* PasswordEncoder是一个接口,该结构被BCryptPasswordEncoder实现了
* BCryptPasswordEncoder类下encode方法,进行加密操作
*
*
* user是实现类,实现了UserDetailsService
* new user(前端传过来的用户名,数据库的密码,权限列表以逗号分隔)
*
* encode:传进去进行加密!!
* 真是定义权限:具体有两个权限:一个admin、一个普通权限
*/
String password = pw.encode(“123”);
return new User(username,password,AuthorityUtils.commaSeparatedStringToAuthorityList
(“admin,normal,ROLE_abc,/main.html,/insert,/delete”));
// abc的角色,增加删除的权限
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder pw;
/**
* 实现接口,那么就需要实现方法:
* 传一个username,就是浏览器传过来的,根据用户名去数据库查询用户
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.查询数据库判断用户名是否存在,如果不存在就会抛异常
if (!"admin".equals(username)){
throw new UsernameNotFoundException("用户名不存在");
}
// 2.如果存在用户则将查询出来的密码尽心解析,与前端比较。
// 或者直接把密码放入构造方法中。
//既然是数据库查询出来的,肯定是加密的密码。
/**
* pw是config/securityConfig下面的bean
* @Bean
* public PasswordEncoder getPw(){
* return new BCryptPasswordEncoder();
* }
* PasswordEncoder是一个接口,该结构被BCryptPasswordEncoder实现了
* BCryptPasswordEncoder类下encode方法,进行加密操作
*
*
* user是实现类,实现了UserDetailsService
* new user(前端传过来的用户名,数据库的密码,权限列表以逗号分隔)
*
* encode:传进去进行加密!!
* 真是定义权限:具体有两个权限:一个admin、一个普通权限
*/
String password = pw.encode("123");
return new User(username,password,AuthorityUtils.commaSeparatedStringToAuthorityList
("admin,normal,ROLE_abc,/main.html,/insert,/delete"));
// abc的角色,增加删除的权限
}
}
3、测试
3、启动项目测试:
localhost:8080/login.html
启动项目后第一次访问这个地址,会自动去security进行控制。如果账号密码成功后。第二次访问这个地址,就直接进入该html文件。
PS:这个不会跳转到controller层!!!
四、自定义登录页面
上面测试,依然运用了Security自带的页面,但是实际项目都是用自己的页面。
1、初步实现自定义登录页面
修改配置类:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//重写对应的方法:
@Override
protected void configure(HttpSecurity http) throws Exception {
//表单提交认证. 指定的页面
http.formLogin().loginPage("/login.html");
}
@Bean
public PasswordEncoder getPw(){
return new BCryptPasswordEncoder();
}
}
2、测试:
访问localhost:8080/login.html会发现直接进入了该页面,并没有出现跳转到security自带页面了。
发现问题:我们/login… /main.html 都可以自由访问。这样失去了权限的意义,如何修改呢???
3、真正实现自定义登录页面
修改配置类
修改配置类中主要是设置哪个页面是登录页面。配置类需要继承WebSecurityConfigurerAdapte,并重写 configure方法。
Securityconfig :
/**
* 我们自定义登录逻辑时,容器内必须有Password实例,
* 所以要写一个配置类。创建config/Securityconfig
*
* 创建好了实例,那么实现UserDetailsService接口
*
* extend WebSecurityConfigurerAdapter
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//重写对应的方法:
@Override
protected void configure(HttpSecurity http) throws Exception {
//表单提交认证.
http.formLogin()
//自定义登录页面
.loginPage("/login.html")
//当发现/login时认为是登录,必须和表单提交的地址是一样的
//去执行UserServiceImpl
.loginProcessingUrl("/login")
//登录成功后跳转页面,post请求
.successForwardUrl("/toMain")
.failureForwardUrl("/toError");
http.authorizeRequests()
//login.html不需要被认证
.antMatchers("/login.html").permitAll()
//登录失败的页面不需要被认证
.antMatchers("/error.html").permitAll()
//所有请求必须都被认证,必须登录后被访问
.anyRequest().authenticated();
//关闭scrf防护,类似防火墙
http.csrf().disable();
}
@Bean
public PasswordEncoder getPw(){
return new BCryptPasswordEncoder();
}
}
successForwardUrl() :登录成功后跳转地址
loginPage() :登录页面
loginProcessingUrl :登录页面表单提交地址,此地址可以不真实存在。
antMatchers() :匹配内容
permitAll() :允许
html 页面:
controller:
@Controller
public class LoginController {
/**
* 成功后跳转页面
* @return
*/
@RequestMapping("/toMain")
public String toMain(){
return "redirect:/main.html";
}
@RequestMapping("/toError")
public String toError(){
return "redirect: error.html";
}
}
service
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder pw;
/**
* 实现接口,那么就需要实现方法:
* 传一个username,就是浏览器传过来的,根据用户名去数据库查询用户
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.查询数据库判断用户名是否存在,如果不存在就会抛异常
if (!"admin".equals(username)){
throw new UsernameNotFoundException("用户名不存在");
}
// 2.如果存在用户则将查询出来的密码尽心解析,与前端比较。
// 或者直接把密码放入构造方法中。
//既然是数据库查询出来的,肯定是加密的密码。
/**
* user是实现类,实现了UserDetailsService
* new user(前端传过来的用户名,数据库的密码,权限列表)
*
* encode:传进去进行加密!!
* 真是定义权限:具体有两个权限:一个admin、一个普通权限
*/
String password = pw.encode("123");
return new User(username,password,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
}
}
html中的Form表单别名
form表单中的 method=post 和name=usename/password都是固定的
那么如何设置别名呢??
<body>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--
拿到登录的form表单
sumbit提交按钮,调用/login方法
method=post/name=usename/password都是固定的
-->
<form action="/login" method="post">
用户名:<input type="text" name="username123" /><br/>
密码:<input type="password" name="password123" /><br/>
<input type="submit" value="登录" />
</form>
</body>
</html>
在SecurtyConfig.java 设置别名:必须和form表单一致。
4、自定义登录成功处理器:
1.跳转成功处理器:
现在都是前后端分离,不会用controller做跳转了
做法:
成功处理器:
去创建Hander类,创建类并实现AuthenticationSuccessHandler接口
失败处理器:
去创建Hander类,创建类并实现AuthenticationFailureHandler接口
hander/myAuthenticationSuccessHander.java
成功处理器:
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private String url;
//有参构造,当有new这个类时,直接传值即可。
public MyAuthenticationSuccessHandler(String url ) {
this.url = url;
}
//实现接口,重写方法
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//response重定义,放入一个URL
response.sendRedirect(url);
}
}
2.跳转失败的处理器:
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private String url;
public MyAuthenticationFailureHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.sendRedirect(url);
}
}
3.在securityConfig的配置类中:
succesHandler传 (AuthenticationSuccessHandler类型)
failureHandler传(AuthenticationFailureHandler类型)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//重写对应的方法:
@Override
protected void configure(HttpSecurity http) throws Exception {
//表单提交认证.
http.formLogin()
//设置login.html表单别名
.usernameParameter("username123")
.passwordParameter("password123")
//自定义登录页面
.loginPage("/login.html")
//当发现/login时认为是登录,必须和表单提交的地址是一样的
//去执行UserServiceImpl
.loginProcessingUrl("/login")
/*登录成功后跳转页面,必须是post请求(post像服务器发送数据)
所以去controller写了跳转页面的方法。
现在都是前后端分离,不会用controller做跳转了
做法:创建Hander类,实现AuthenticationSuccessHandler接口
succesHandler传: AuthenticationSuccessHandler接口类型
*/
.successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com"))
.failureHandler(new MyAuthenticationFailureHandler("error.html"));
/* 登录成功后的处理器,不能和successForwardUrl共存
forwardurl不能在外跳转。
//.successForwardUrl("/toMain")
//.failureForwardUrl("/toError");
*/
http.authorizeRequests()
//login.html不需要被认证
.antMatchers("/login.html").permitAll()
//登录失败的页面不需要被认证
.antMatchers("/error.html").permitAll()
//所有请求必须都被认证,必须登录后被访问
.anyRequest().authenticated();
//关闭scrf防护,类似防火墙
http.csrf().disable();
}
4.测试:
这次跳转并没有去controller进行跳转,而是通过处理器进行跳转。
成功跳转到:http://www.baidu.com
跳转成功:
错误的密码,跳转到error.html:
5、http.authorizeRequests()…请求授权下的注解
1.anyRequest()详解:
除了上面定义多个antMatchers
任何请求 必须都被认证,也就是说必须登录后才能被访问
.anyRequest().authenticated();
2.antMatchers详解:(最常用的放行)
方法如下:
public c antMatchaes(Sgring... antPatterns)
参数是不定向参数,每个参数是一个ant表达式,用于匹配URL规则。
http.authorizeRequests()
/**
* antMatchers 匹配内容--》单独匹配
* login.html不需要被认证
* permitAll允许访问,
* -----------
* antMatchers的讲解:
* public C antMatchers(String... antPatterns) { //可以加多个目录
* Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
* return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns));
* }
* ?匹配一个字符
* *:匹配0个多个字符
* **:匹配0个多个目录
*
* 一般静态资源就会放行(js/html/img/css)
*
*/
.antMatchers("/js/**","css/**","images/**").permitAll()
.antMatchers("/login.html").permitAll()
//登录失败的页面不需要被认证
.antMatchers("/error.html").permitAll()
/***
* 除了上面定义多个antMatchers
* 任何请求 必须都被认证,也就是说必须登录后才能被访问
*/
.anyRequest().authenticated();
测试:放行静态资源:
3.regexMatchers()详解:
使用正则表达式进行匹配。和 antMatchers() 主要的区别就是参数, antMatchers() 参数是 ant 表达式,regexMatchers() 参数是正则表达式。
演示所有以.js 结尾的文件都被放行。
.regexMatchers( ".+[.]js").permitAll()
- 两个参数时使用方式:
无论是 antMatchers() 还是 regexMatchers() 都具有两个参数的方法,其中第一个参数都是 HttpMethod ,表示请求方式,当设置HttpMethod 后表示只有设定的特定的请求方式才执行对应的权限设置。
1、可也任意指定请求方式:但是必须请求类型是匹配的。要不然也会拦截
2、例子中:去controller请求。这样访问时,8080/demo 。返回一个字符串
4.mvcMatchers()详解:
mvcMatchers()适用于配置了 servletPath 的情况。
-
servletPath 就是所有的 URL 的统一前缀。在 SpringBoot 整合SpringMVC 的项目中可以在
application.properties 中添加下面内容设置 ServletPath
spring.mvc.servlet.path=/xxxx
-
在 Spring Security 的配置类中配置 .servletPath() 是 mvcMatchers()返回值特有的方法,antMatchers()和regexMatchers()没有这个方法。在 servletPath() 中配置了 servletPath 后,mvcMatchers()直接写 SpringMVC中@RequestMapping()中设置的路径即可。
.mvcMatchers("/demo").servletPath("/xxxx").permitAll()
-
如果不习惯使用 mvcMatchers()也可以使用 antMatchers(),下面代码和上面代码是等效
.antMatchers("/xxxx/demo").permitAll()
测试:
http.authorizeRequests()
// 在yml配置了:所有的URL都需要前面加上xxxx,才能访问
//.mvcMatchers("/demo").servletPath("/xxxx").permitAll()
// 上面和下面等同
.antMatchers("/xxxx/demo").permitAll()
//登录失败的页面不需要被认证
.antMatchers("/error.html").permitAll()
5.内置访问控制方法
Spring Security 匹配了 URL 后调用了 permitAll() 表示不需要认证,随意访问。在 Spring Security 中提供了多种内置控制。
- permitAll()《-----》经常用
permitAll()表示所匹配的 URL 任何人都允许访问。 - authenticated() 《-----》经常用
authenticated()表示所匹配的 URL 都需要被认。 - anonymous()
anonymous()表示可以匿名访问匹配的URL。和permitAll()效果类似,只是设置为 anonymous()的 url 会执行 filter
链中 - denyAll()
denyAll()表示所匹配的 URL 都不允许被访问。 - rememberMe() 《-----》经常用
被“remember me”的用户允许访问。登录界面的“记住我”,只有点了“记住我”才能进行访问。 - fullyAuthenticated()
如果用户不是被 remember me 的,才可以访问。如果点了“记住我”就不能访问,只能手动输入账号密码才能点进来。
6.用户和角色权限判断
除了之前讲解的内置权限控制。Spring Security 中还支持很多其他权限控制。这些方法一般都用于用户已经被认证后(登录成功以后,有无权限去干的一件事情),判断用户是否具有特定的要求。 【例如:网站会员,有无权限使用某些操作】
-
hasAuthority(String)
判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建 User 对象时指定的。下图中 admin和normal 就是用户的权限。admin和normal 严格区分大小写。
-
在配置类中通过 hasAuthority(“admin”)设置具有 admin 权限时才能访问。
只有admin权限才能访问main1.html
.antMatchers("/main1.html").hasAuthority("admin")
- hasAnyAuthority(String …) 多个权限。。
如果用户具备给定权限中某一个,就允许访问。
下面代码中由于大小写和用户的权限不相同,所以用户无权访问
.antMatchers("/main1.html").hasAnyAuthority("adMin","admiN")
测试:
从login.html登录成功后,任何权限都能访问到main,main有个链接到Main1。
第一次:我设为:admin1权限才能登录,我用admin登录报错显示:403没有权限。
第二次: 我设为:admin/admin1权限才能登录,可以登录了。
// hasauthority只有admin权限才能登录:
//.antMatchers("/main1.html").hasAuthority("admin1")
// hasanyauthority只有admin,admin1的权限才能登录。any可以指定多个。
.antMatchers("/main1.html").hasAnyAuthority("admin1","admin")
- hasRole(String)
如果用户具备给定角色就允许访问。否则出现 403。
参数取值来源于自定义登录逻辑 UserDetailsService 实现类中创建 User 对象时给 User 赋予的授权。
在给用户赋予角色时角色需要以: ROLE_开头 ,后面添加角色名称。例如:ROLE_abc 其中 abc 是角色名,ROLE_是固定的字符开头。
使用 hasRole()时参数也只写 abc 即可。否则启动报错。
给用户赋予角色:
在配置类中直接写 abc 即可。
```.antMatchers("/main1.html").hasRole(“abc”)``
7.IP地址判断
通过IP地址对权限的个控制。项目的后台管理系统,只能在指定的服务器进行登录操作。这台服务器地址是固定的。那么就可以通过ip地址权限控制。
.antMatchers("/main1.html").hasIpAddress("127.0.0.1")
当我们绑定了127.0.0.1,用locahost:登录成功后,从main跳转到main1时,发现报错403了
现在用127.0.0.1去跳转main1.html,发现成功了:
(忽略页面上面的话语)
6. 解决403权限不足,页面展示:
而在实际项目中可能都是一个异步请求,显示上述效果对于用户就不是特别友好了。Spring Security 支持自定义权
限受限。
新建类实现 AccessDeniedHandler
//组成
@Component
public class MyAccessDeniedHandle implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//1.设置状态码403
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
//3.相应的请求头
response.setHeader("Content-Type","application/json;charset=utf-8");
//2.写一段话,相应给浏览器
PrintWriter writer = response.getWriter();
writer.write("{\"status\":\"error\",\"msg\":\"权限不足\"}");
writer.flush();
writer.close();
}
}
修改配置类
配置类中重点添加异常处理器。设置访问受限后交给哪个对象进行处理。
myAccessDeniedHandler 是在配置类中进行自动注入的。
@Autowired
private MyAccessDeniedHandle myAccessDeniedHandle;
//异常处理
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
测试进入:main1.html,不在是403报错了,而是友好提示:
7.基于表达式的访问控制
1、access()方法使用
之前学习的登录用户权限判断实际上底层实现都是调用access(的内置表达式)
可以通过 access() 实现和之前学习的权限控制完成相同的功能。
以 hasRole 和 和 permitAll 举例
2、access实现自定义表达式
虽然这里面已经包含了很多的表达式(方法)但是在实际项目中很有可能出现需要自己自定义逻辑的情况。
判断登录用户是否具有访问当前 URL 权限。
新建接口及实现类
MyService.java
public interface MyService {
/**
* 允许的意思
* request:是为了去拿对应的主体和权限。
* authentication:权限的意思
*/
boolean hasPermission(HttpServletRequest request, Authentication authentication);
}
实现接口:MyServiceImpl
@Service
public class MyServiceImpl implements MyService{
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
//获得主体,就是User对象
Object obj = authentication.getPrincipal();
if(obj instanceof UserDetails){
//如果
UserDetails userDetails= (UserDetails) obj;
//得到权限
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
//判断权限,是否包含URL.包含一个
return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
}
return false;
}
}
修改配置类:
在 access 中通过@bean的id名.方法(参数)的形式进行调用配置类中修改如下:
http.authorizeRequests()
.antMatchers("/login.html").permitAll()
/***
* 除了上面定义多个antMatchers
* 任何请求 必须都被 认证,也就是说必须登录后才能被访问
* ---
* access实现自定义表达式
* 做一个demo,判断登录到用户是否有访问当前页面的权限。
*/
//.anyRequest().authenticated();
.anyRequest().access("@myServiceImpl.hasPermission(request,authentication)");
8、基于注解的访问控制
在 Spring Security 中提供了一些访问控制的注解。这些注解都是默认是都不可用的,需要通过@EnableGlobalMethodSecurity 进行开启后使用。
如果设置的条件允许,程序正常执行。如果不允许会报 500
这些注解可以写到 Service 接口或方法上,也可以写到 Controller或 Controller 的方法上。通常情况下都是写在控制器方法上的,控制接口URL是否允许被访问。
1、secured判断是否具有角色
-
@Secured
@Secured 是专门用于判断是否具有角色的。能写在方法或类上。参数要以 ROLE_开头。 -
开启注解
在 启 动 类 ( 也 可 以 在 配 置 类 等 能 够 扫 描 的 类 上 ) 上 添 加
@EnableGlobalMethodSecurity(securedEnabled = true)
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringsecurityDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringsecurityDemoApplication.class, args);
}
}
在控制器方法上添加@Secured 注解
/**
* 成功后跳转页面
* @return
*/
@Secured("ROLE_abc")
@RequestMapping("/toMain")
public String toMain(){
return "redirect:/main.html";
}
配置类
Override
protected void configure(HttpSecurity http) throws Exception {
//表单提交
http.formLogin()
//自定义登录页面
.loginPage("/login.html")
//当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
.loginProcessingUrl("/login")
//登录成功后跳转页面,POST请求
.successForwardUrl("/toMain")
//url拦截
http.authorizeRequests()
//login.html不需要被认证
.antMatchers("/login.html").permitAll()
//所有请求都必须被认证,必须登录后被访问
.anyRequest().authenticated();
//关闭csrf防护
http.csrf().disable();
}
2、@PreAuthorize访问方法在执行之前先判断权限
@PreAuthorize 表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和
access()方法参数取值相同,都是权限表达式。
开启注解
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringsecurityDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringsecurityDemoApplication.class, args);
}
}
添加@PreAuthorize
在控制器方法上添加@PreAuthorize,参数可以是任何 access()支持的表达式
/**成功后跳转页面
* @return
* */
@PreAuthorize("hasRole('ROLE_abc')")
@RequestMapping("/toMain")
public String toMain(){
return "redirect:/main.html";
}
9、RememberMe功能实现(记住我)
Spring Security 中 Remember Me 为“记住我”功能,用户只需要在登录时添加 remember-me复选框,取值为true。Spring Security 会自动把用户信息存储到数据源中,以后就可以不登录进行访问.
添加依赖
Spring Security 实 现 Remember Me 功 能 时 底 层 实 现 依 赖Spring-JDBC,所以需要导入 Spring-JDBC。现在使用 MyBatis 框架很多,而很少直接导入 spring-jdbc,所以此处导入 mybatis 启动器同时还需要添加 MySQL 驱动
配置数据源
在 application.properties 中配置数据源。请确保数据库中已经存在shop数据库
spring.datasource.driver-class-name= com.mysql.cj.jdbc.Driver
spring.datasource.url= jdbc:mysql://localhost:3306/security?
useUnicode=true&characterEncoding=UTF8&serverTimezone=Asia/Shanghai
spring.datasource.username= root
spring.datasource.password= root
编写配置
ServiceConfig.java
//记住我
http.rememberMe()
//失效时间:10秒,单位秒
.tokenValiditySeconds(10)
//自定义登录逻辑
.userDetailsService(userDetailsService)
//持久层对象
.tokenRepository(persistentTokenRepository);
@Bean
public PersistentTokenRepository getPersistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//自动建表,第一次启动时需要,第二次启动时注释掉
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
添加网页记住我:
<form action="/login" method="post">
用户名:<input type="text" name="username" /><br/>
密码:<input type="password" name="password" /><br/>
记住我:<input type="checkbox" name="remember-me" value="true" /><br/>
<input type="submit" value="登录" />
</form>
测试:
- 数据库的变化:
创建数据库时:只是创建了一个数据库。并没有创建表。
当运行程序时,发现数据库多了表,但没有内容。
输入账号和密码后:
- 登录变化:
第一次登录完成后,可以看到数据库有了数据。然后我们关闭浏览器,从新打开浏览器,直接到localhost:8080/main.html。发现不用登录了,可以直接到达该页面。
10、Thymeleaf中SpringSecurity的使用
Spring Security 可以在一些视图技术中进行控制显示效果。例如: JSP 或 Thymeleaf 。在非前后端分离且使用Spring Boot 的项目中多使用 Thymeleaf 作为视图展示技术。
Thymeleaf 对 Spring Security 的 支 持 都 放 在 thymeleaf-extras-springsecurityX 中,目前最新版本为 5。所以需要在项目中添加此 jar 包的依赖和 thymeleaf 的依赖。
<!--thymeleaf springsecurity5 依赖-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!--thymeleaf依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
在 html 页面中引入 thymeleaf 命名空间和 security 命名空间
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
新建demo.html
在项目 resources 中新建 templates 文件夹,在 templates 中新建demo.html 页面
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录账号:<span sec:authentication="name"></span><br/>
登录账号:<span sec:authentication="principal.username"></span><br/>
凭证:<span sec:authentication="credentials"></span><br/>
权限和角色:<span sec:authentication="authorities"></span><br/>
客户端地址:<span sec:authentication="details.remoteAddress"></span><br/>
sessionId:<span sec:authentication="details.sessionId"></span><br/>
</body>
</html>
编写Controller
thymeleaf 页面需要控制转发,在控制器类中编写下面方法
RequestMapping("/demo")
public String demo(){
return "demo";
}
2、通过权限和角色,在页面展示:
在页面中根据用户权限和角色判断页面中显示的内容。
可以控制哪些按钮,被哪些权限、角色使用。
11、退出
销毁http的对象
清除认证
退出成功的处理器。
在main.html中添加:
<a href="/logout">退出</a>
当我们只在HTML中添加了代码,并没有去config中添加配置代码:
在securityConfig中添加:自定义登录退出页面
//退出
http.logout()
//必须和main.html退出登录的一样("/logout"). 默认就是logout
//.logoutUrl("/login")
//跳出去登录页面。
.logoutSuccessUrl("/login.html");
五、csrf跨站请求伪造
在配置类中一直存在这样一行代码: http.csrf().disable(); 如果没有这行代码导致用户无法被认证。这行代码的含义是:关闭 csrf 防护。
学习阶段直接关闭csrf。
工作在登录页面中:去服务器中取到生成的token,然后在访问的时候再带回服务器。服务器会判断带回来的token和生成的token是否一致。一致则访问。
1、什么是CSRF跨站请求伪造
例子说明跨站请求伪造:
用户A通过网站给B转钱。正常操作
用户C伪装用户A,通过网站给C转钱。非正常操作。
也就是说用户C把用户A的钱,转给自己了。
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“OneClick Attack” 或者 Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
跨域:只要网络协议,ip 地址,端口中任何一个不相同就是跨域请求。
客户端与服务进行交互时,由于 http 协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id 可能被第三方恶意劫持,通过这个session id 向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。
2、Spring Security中的CSR
从 Spring Security4开始CSRF防护默认开启。默认会拦截请求。进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为 _csrf 值为token(token 在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。
1、修改配置类
在配置类中注释掉 CSRF 防护失效
//关闭csrf防护
// http.csrf().disable();
当我们关闭权限运行,在正确密码登录:发现无法登陆。
是因为:我们在登录访问的时候,要求携带参数名csrf,token值参数。如果没有的话,那就不让进行正常访问。
2、新建在/templates/login.html
添加隐藏域:
取服务器的{_csrf.token}
判断:如果有放进去,没有则空。
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<-- 隐藏域 -->
<form action="/login" method="post">
<input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/>
用户名:<input type="text" name="username123" /><br/>
密码:<input type="password" name="password123" /><br/>
记住我:<input type="checkbox" name="remember-me" value="true"/><br/>
<input type="submit" value="登录" />
</form>
</body>
</html>
登录进来了以后:_csrf:就是服务器生成的token
六:该学习过程涉及的代码:
1、config/SecurityConfig
/**
* 我们自定义登录逻辑时,容器内必须有Password实例,
* 所以要写一个配置类。创建config/SecurityConfig
*
* 创建好了实例,那么实现UserDetailsService接口
*
* extend WebSecurityConfigurerAdapter
*
*
* successForwardUrl() :登录成功后跳转地址
* loginPage() :登录页面
* loginProcessingUrl :登录页面表单提交地址,此地址可以不真实存在。
* antMatchers() :匹配内容
* permitAll() :允许
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAccessDeniedHandle myAccessDeniedHandle;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
public DataSource dataSource;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
//重写对应的方法:
@Override
protected void configure(HttpSecurity http) throws Exception {
//表单提交认证.
http.formLogin()
//设置login.html表单别名
.usernameParameter("username123")
.passwordParameter("password123")
//自定义登录页面
// .loginPage("/login.html")
//关闭crsf,去controller访问跳转
.loginPage("/showLogin")
//当发现/login时认为是登录,必须和表单提交的地址是一样的
//去执行UserServiceImpl
.loginProcessingUrl("/login")
/*登录成功后跳转页面,必须是post请求(post像服务器发送数据)
所以去controller写了跳转页面的方法。
现在都是前后端分离,不会用controller做跳转了
做法:创建Handler类,实现AuthenticationSuccessHandler接口
successHandler传: AuthenticationSuccessHandler接口类型
.successHandler(new MyAuthenticationSuccessHandler("main.html"))
.failureHandler(new MyAuthenticationFailureHandler("error.html"));*/
/* 登录成功后的处理器,不能和successForwardUrl共存
forwardUrl不能在外跳转。
---
此时测试:serured注解:跳转到controller进行跳转:*/
.successForwardUrl("/toMain")
.failureForwardUrl("/toError");
//授权认证
http.authorizeRequests()
/*
* antMatchers 匹配内容--》单独匹配
* login.html不需要被认证
* permitAll允许访问,
* -----------
* antMatchers的讲解:
* public C antMatchers(String... antPatterns) { //可以加多个目录
* Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
* return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns));
* }
* ?匹配一个字符
* *:匹配0个多个字符
* **:匹配0个多个目录
*
* 一般静态资源就会放行(js/html/img/css)
* antMatchers:下面的网页都可以访问。
*/
//.antMatchers("/js/**","css/**","images/**").permitAll()
//.antMatchers("/login.html").permitAll()
//放行showLogin
.antMatchers("/showLogin").permitAll()
//mvc路径判断:
// 在yml配置了:所有的URL都需要前面加上xxxx,才能访问.
//.mvcMatchers("/demo").servletPath("/xxxx").permitAll()
// 上面和下面等同
// .antMatchers("/xxxx/demo").permitAll()
//权限判断:
// hasauthority只有admin权限才能登录:
//.antMatchers("/main1.html").hasAuthority("admin1")
// hasanyauthority只有admin,admin1的权限才能登录。any可以指定多个。
//.antMatchers("/main1.html").hasAnyAuthority("admin1","admin")
//角色判断:
//.antMatchers("/main1.html").hasRole("abc")
//固定ip地址访问:
//.antMatchers("/main1.html").hasIpAddress("127.0.0.1")
//登录失败的页面不需要被认证
//.antMatchers("/error.html").permitAll()
.antMatchers("/error.html").access("permitAll()")
/*
* 除了上面定义多个antMatchers
* 任何请求 必须都被 认证,也就是说必须登录后才能被访问
* ---
* access实现自定义表达式
* 做一个demo,判断登录到用户是否有访问当前页面的权限。
*/
.anyRequest().authenticated();
// access实现自定义表达式
//.anyRequest().access("@myServiceImpl.hasPermission(request,authentication)");
//关闭scrf防护,类似防火墙
// http.csrf().disable();
//异常处理:
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandle);
//记住我
http.rememberMe()
//登录逻辑交给哪个对象
.userDetailsService(userDetailsService)
// 持久层对象
.tokenRepository(persistentTokenRepository);
//退出
http.logout()
//必须和main.html退出登录的一样("/logout")
//.logoutUrl("/logout")
//跳出去登录页面。
.logoutSuccessUrl("/login.html");
}
@Bean
public PasswordEncoder getPw(){
return new BCryptPasswordEncoder();
}
@Bean
public PersistentTokenRepository getPersistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//自动建表,第一次启动时需要,第二次启动时注释掉
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
2、controller
Controller/LoginController
//@RestController
@Controller
public class LoginController {
/*
* 登录方法:
*
@RequestMapping("/login")
public String login(){
System.out.println("执行了登录方法");
//重定向到主页:
return "redirect: main.html";
}*/
//在准备两个HTML:login.html / main.html
// 当浏览器访问“/login"时,给浏览器返回main.html
/**
* 成功后跳转页面
* @return
* 角色:
*/
//@Secured("ROLE_abc")
//preauthorize的表达式允许ROLE_开头,也可以不开头,配置类不允许开头
@RequestMapping("/toMain")
@PreAuthorize("hasRole('abc')")
public String toMain(){
return "redirect:/main.html";
}
@RequestMapping("/toError")
public String toError(){
return "redirect:/error.html";
}
@RequestMapping("/demo")
public String demo(){
return "demo";
}
/**
* 关闭csrf
* 去templates的login.html去访问
* @return
*/
@RequestMapping("/showLogin")
public String showLogin(){
return "login";
}
}
3、hendler: 成功跳转、失败跳转、403跳转:
hendler/MyAccessDeniedHandle 403跳转
//组成
@Component
public class MyAccessDeniedHandle implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//1.设置状态码403
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
//3.相应的请求头
response.setHeader("Content-Type","application/json;charset=utf-8");
//2.写一段话,相应给浏览器
PrintWriter writer = response.getWriter();
writer.write("{\"status\":\"error\",\"msg\":\"权限不足\"}");
writer.flush();
writer.close();
}
}
hendler/MyAuthenticationFailureHandle 失败跳转
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private String url;
public MyAuthenticationFailureHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.sendRedirect(url);
}
}
hendler/MyAuthenticationFailureHandle 成功跳转
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private String url;
//有参构造,当有new这个类时,直接传值即可。
public MyAuthenticationSuccessHandler(String url ) {
this.url = url;
}
//实现接口,重写方法
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//response重定义,放入一个URL
response.sendRedirect(url);
}
}
4、service
service/MyService
public interface MyService {
/**
* 允许的意思
* request:是为了去拿对应的主体和权限。
* authentication:权限的意思
*/
boolean hasPermission(HttpServletRequest request, Authentication authentication);
}
MyServiceImpl
@Service
public class MyServiceImpl implements MyService{
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
//获得主体,就是User对象
Object obj = authentication.getPrincipal();
if(obj instanceof UserDetails){
//如果
UserDetails userDetails= (UserDetails) obj;
//得到权限
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
//判断权限,是否包含URL.包含一个
return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
}
return false;
}
}
UserDetailsServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder pw;
/**
* 实现接口,那么就需要实现方法:
* 传一个username,就是浏览器传过来的,根据用户名去数据库查询用户
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.查询数据库判断用户名是否存在,如果不存在就会抛异常
if (!"admin".equals(username)){
throw new UsernameNotFoundException("用户名不存在");
}
// 2.如果存在用户则将查询出来的密码尽心解析,与前端比较。
// 或者直接把密码放入构造方法中。
//既然是数据库查询出来的,肯定是加密的密码。
/**
* pw是config/securityConfig下面的bean
* @Bean
* public PasswordEncoder getPw(){
* return new BCryptPasswordEncoder();
* }
* PasswordEncoder是一个接口,该结构被BCryptPasswordEncoder实现了
* BCryptPasswordEncoder类下encode方法,进行加密操作
*
*
* user是实现类,实现了UserDetailsService
* new user(前端传过来的用户名,数据库的密码,权限列表以逗号分隔)
*
* encode:传进去进行加密!!
* 真是定义权限:具体有两个权限:一个admin、一个普通权限
*/
String password = pw.encode("123");
return new User(username,password,AuthorityUtils.commaSeparatedStringToAuthorityList
("admin,normal,ROLE_abc,/main.html,/insert,/delete"));
// abc的角色,增加删除的权限
}
}
5、static/*.html
6、templates/*.html