目录
SpringSecurity简介
SpringSecurity是一个强大的可高度定制的认证和授权框架,对于Spring应用来说它是一套Web安全标准。
SpringSecurity注重于为Java应用提供认证和授权功能,像所有的Spring项目一样,它对自定义需求具有强大的扩展性。
SpringSecurity初体验
1.新建项目
使用IDEA创建SpringBoot项目,引入SpringSecurity和web依赖
若不是IDEA,在新建的SpringBoot项目依赖中添加如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2.添加测试内容
新建测试controller类
@RestController
public class TestController {
@GetMapping("/test")
public String test() {
return "Hello World!";
}
@GetMapping("/success")
public String success() {
return "success!";
}
@GetMapping("/failure")
public String failure() {
return "failure!";
}
}
3.启动测试
直接启动项目,启动之后,我们在控制台中看见SpringSecurity生成的一个UUID登录密码,登录用户默认的是user
Using generated security password: 6de83077-9657-4f8d-92d3-d1e651cf34a4
至于为什么默认的登录用户时user,我们可以进入SecurityProperties类中查看Security默认配置信息,如下:
//SecurityProperties中内部类User部分源码
private String name = "user";
private String password = UUID.randomUUID().toString();
private boolean passwordGenerated = true;//是否生成密码
然后我们通过浏览器访问:http://localhost:8080/test ,页面会自动重定向到默认的登录页面
输入用户名/密码,点击登录,之后访问成功
4.配置登录用户
由于SpringSecurity默认的登录用户密码在每次启动时都会重新生成,我们可以再配置文件中配置固定的用户名和密码
spring.security.user.name=symon
spring.security.user.password=123456
然后重新启动,就能使用自定义的用户名和密码登录
自此一个简单的Security集成就结束了,接下来我们开始入门学习Security的其他配置
SpringSecurity配置自定义登录页面
1.配置多个登录用户
在上面的基础上,新建SecurityConfig配置类,并继承WebSecurityConfigurerAdapter ,重写其中configure方法即可进行自定义配置,下面是个简单配置多个登录用户:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("symon").password("1234567").roles("root")
.and().withUser("test").password("1234567").roles("user");
}
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
其中:
passwordEncoder()方法配置密码加密方式,这里为了方便采用了NoOpPasswordEncoder不加密配置
configure(AuthenticationManagerBuilder auth)方法中配置了inMemoryAuthentication在内容中定义用户,使用and进行连接配置多个用户
再次启动项目,访问/test接口,使用代码中配置的用户名密码进行登录即可登录成功,此时我们发现代码中的配置会覆盖掉properties配置文件中的配置
2.配置自己的登录页面
一般,在项目中我们不会使用Security默认的登录页面,所需我们需要在登陆时配置自己写的登录页面
继续在SecurityConfig重写configure(HttpSecurity http)方法,configure(WebSecurity web)方法:
@Override
public void configure(WebSecurity web) throws Exception {
//用来忽略URL地址,被忽略的URL不会被Security拦截,一般项目中的静态文件需要忽略。
web.ignoring().antMatchers("/js/**", "/css/**","/images/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html") //设置登录页面
.loginProcessingUrl("/login") //设置登录请求接口
.usernameParameter("name") //配置登录用户参数名,默认值username
.passwordParameter("pass") //配置登录密码参数名,默认值password
.defaultSuccessUrl("/success", true) //配置登录成功跳转地址,重定向
.failureUrl("/failure") //配置登录失败跳转地址,重定向
.permitAll() //允许以上页面和接口,不被拦截
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html") //配置登出成功跳转地址
.deleteCookies() //清除cookie
.clearAuthentication(true) //清除认证信息 默认true
.invalidateHttpSession(true) //使HttpSession失效 默认true
.permitAll()
.and()
.csrf().disable();
}
相关配置解释已在注释中
补充:
successForwardUrl(),failureForwardUrl()也可进行成功和失败跳转,不过该方式是转发
然后将自己写的登录页面放在resources/static文件夹下,我这里写了一个简单的登录页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<form action="/login" method="post">
<div>
<label for="name">用户名</label>
<input type="text" name="name" id="name">
</div>
<div>
<label for="pass">密码</label>
<input type="password" name="pass" id="pass">
</div>
<div>
<button type="submit">
<span>登录</span>
</button>
</div>
</form>
</body>
</html>
启动项目之后,浏览器访问/test接口,就会重定向到自己的登录页面,输入用户名密码即可登录成功
SpringSecurity前后端分离
在前后端分离的今天,我们逐渐采用JSON进行数据交互,下面我们开始学习基于 session 的前后端分离认证。
1.登陆处理
之前登录成功或失败是通过defaultSuccessUrl和failureUrl来重定向跳转页面,前后端分离之后,无论登录成功或失败服务端都只返回JSON信息
登录成功处理
successHandler是登录成功的处理
.successHandler((req, resp, authentication) -> {
resp.setContentType(CONTENT_TYPE);
PrintWriter out = resp.getWriter();
out.write(JSON.toJSONString(ResponseDTO.success("登录成功!")));
out.flush();
out.close();
})
其中:
successHandler 方法的参数是一个 AuthenticationSuccessHandler 对象,实现其 onAuthenticationSuccess方法即可进行登录成功的操作
onAuthenticationSuccess 方法有三个参数,分别是:HttpServletRequest、HttpServletResponse、Authentication
HttpServletRequest和HttpServletResponse可以用来做服务端和客户端跳转,HttpServletResponse也可用来返回 JSON 数据, Authentication包含登录用户的认证信息
登陆失败处理
failureHandler是登录失败的处理
.failureHandler((req, resp, exception) -> {
resp.setContentType(CONTENT_TYPE);
PrintWriter out = resp.getWriter();
String exeMsg = "登录失败!";
if (exception instanceof LockedException) {
exeMsg = "账户已被锁定!";
} else if (exception instanceof CredentialsExpiredException) {
exeMsg = "密码已过期!";
} else if (exception instanceof AccountExpiredException) {
exeMsg = "账户已过期!";
} else if (exception instanceof DisabledException) {
exeMsg = "账户已被禁用!";
} else if (exception instanceof BadCredentialsException) {
exeMsg = "用户名或者密码输入错误,请重新输入!";
}
out.write(JSON.toJSONString(ResponseDTO.error(exeMsg)));
out.flush();
out.close();
})
其中:
failureHandler方法的参数是一个AuthenticationFailureHandler对象,实现其onAuthenticationFailure方法即可进行登录失败的操作
onAuthenticationFailure与登录成功参数类似,不过第三个参数是Exception异常对象,根据异常类型,进行不同的处理
2.注销处理
同样的注销也有类似的回调,logoutSuccessHandler用来处理注销成功的操作,参数和登录类似
.logoutSuccessHandler((req, resp, auth) -> {
resp.setContentType(CONTENT_TYPE);
PrintWriter out = resp.getWriter();
out.write(JSON.toJSONString(ResponseDTO.success("注销成功!再见!")));
out.flush();
out.close();
})
3.未登录处理
如果用户没有登录就访问一个需要认证后才能访问的页面,这个时候,我们需要给前端一个未登录的提醒,让前端来进行页面跳转
.exceptionHandling()
.authenticationEntryPoint((req, resp, exception) -> {
resp.setContentType(CONTENT_TYPE);
PrintWriter out = resp.getWriter();
out.write(JSON.toJSONString(ResponseDTO.unauthenticated("未登录,请重新登录!")));
out.flush();
out.close();
})
接下来开始实操
4.编程测试
首先我们新建一个项目,除了要引入security和web依赖之外,这里引入了lombok和fastjson方便操作
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
创建SecurityConfig配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public static final String CONTENT_TYPE = "application/json;charset=utf-8";
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler((req, resp, auth) -> {
resp.setContentType(CONTENT_TYPE);
PrintWriter out = resp.getWriter();
out.write(JSON.toJSONString(ResponseDTO.success("登录成功!")));
out.flush();
out.close();
})
.failureHandler((req, resp, exception) -> {
resp.setContentType(CONTENT_TYPE);
PrintWriter out = resp.getWriter();
String exeMsg = "登录失败!";
if (exception instanceof LockedException) {
exeMsg = "账户已被锁定!";
} else if (exception instanceof CredentialsExpiredException) {
exeMsg = "密码已过期!";
} else if (exception instanceof AccountExpiredException) {
exeMsg = "账户已过期!";
} else if (exception instanceof DisabledException) {
exeMsg = "账户已被禁用!";
} else if (exception instanceof BadCredentialsException) {
exeMsg = "用户名或者密码输入错误,请重新输入!";
}
out.write(JSON.toJSONString(ResponseDTO.error(exeMsg)));
out.flush();
out.close();
})
.and()
.logout()
.logoutSuccessHandler((req, resp, auth) -> {
resp.setContentType(CONTENT_TYPE);
PrintWriter out = resp.getWriter();
out.write(JSON.toJSONString(ResponseDTO.success("注销成功!再见!")));
out.flush();
out.close();
})
.permitAll()
.and()
.exceptionHandling()
.authenticationEntryPoint((req, resp, exception) -> {
resp.setContentType(CONTENT_TYPE);
PrintWriter out = resp.getWriter();
out.write(JSON.toJSONString(ResponseDTO.unauthenticated("未登录,请重新登录!")));
out.flush();
out.close();
})
.and().csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("symon").password("1234567").roles("root")
.and().withUser("test").password("1234567").roles("user");
}
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
其中:
ResponseDTO是我自定义的返回类,其中的字段如下,根据自己需要创建
private Integer code;
private String msg;
private T data;
然后新建测试接口,启动项目使用postman来测试
@GetMapping("/test")
public String test() {
return "Hello World!";
}
当直接访问/test接口时,会返回未登录信息
调用登录接口,输入正确用户名密码,返回登录成功信息
调用登录接口,输入错误用户名密码,返回登录失败信息
登录成功之后,再访问/test测试接口,即可访问成功
调用注销接口,返回注销成功信息
讲完了,security前后端认证的相关操作,接下来我们说一下security的授权
SpringSecurity授权
所谓的授权,就是用户如果要访问某一个资源,我们要去检查用户是否具备这样的权限,如果具备就允许访问,如果不具备,则不允许访问
下面我们在前面认证的代码基础上进行实操
1.给用户添加角色
在前面的配置中,基于内存来定义用户,其中roles()方法就是用来给用户添加角色
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("symon").password("1234567").roles("root")
.and().withUser("test").password("1234567").roles("user");
}
2.准备测试资源
新建两个测试接口,代表两种访问资源
@GetMapping("/user/test")
public String user(){
return "user权限";
}
@GetMapping("/root/test")
public String root(){
return "root权限";
}
3.给资源配置访问权限
.antMatchers("/user/**").hasRole("user")
.antMatchers("/root/**").hasRole("root")
anyRequest 一定要配置在antMatchers之后,否则启动报错
antMatchers中通配符的含义
* * 匹配多层url路径
* 匹配一层url路径
? 匹配任意单个字符
4.测试
启动项目测试,使用symon登录,分别访问 /test,/root/test以及 /user/test三个接口,结果如下:
/test 没有配置访问权限,访问成功。
/root/test 需要root权限,访问成功。
/user/test 需要user权限,访问失败。
这里就不截图了,可以自己测试效果;再使用test登录,对比两个用户访问的结果
5.权限继承(角色继承)
在实际开发中,user角色能够访问的资源,root角色也是能够访问,所以我们就要用到角色继承
在配置类中填加如下配置:
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_root > ROLE_user");
return hierarchy;
}
重启项目,依然使用symon登录并访问/user/test,我们发现就可以访问成功
6.无权限(403 Forbidden)处理
在上面的测试中,我们发现在访问一个,无权访问的接口时,返回的结果如下:
{
"timestamp": "2020-07-20T14:10:33.331+00:00",
"status": 403,
"error": "Forbidden",
"message": "",
"path": "/root/test"
}
但是这个结果并不是我们想要的JSON数据格式
前面认证时我们定义了自己的返回JSON格式,其中在未登录处理的配置时,我们还可以进行无权限返回配置,配置和未登录处理配置类似:
.accessDeniedHandler((req, resp, exception) -> {
resp.setContentType(CONTENT_TYPE);
PrintWriter out = resp.getWriter();
out.write(JSON.toJSONString(ResponseDTO.unauthorized("无权限!")));
out.flush();
out.close();
})
添加配置之后,重启项目
当访问登录用户无权访问的资源时,即可返回自己的JSON格式
{
"code": 403,
"msg": "无权限!"
}
后面我们将继续学习Security