简介
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。
Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,SpringSecurity安全性的真正强大之处在于它可以轻松地扩展以满足定制需求。
从官网的介绍中可以知道这是一个权限框架。想我们之前做项目是没有使用框架是怎么控制权限的?对于权限 一般会细分为功能权限,访问权限,和菜单权限。代码会写的非常的繁琐,冗余。
怎么解决之前写权限代码繁琐,冗余的问题,一些主流框架就应运而生,而Spring Security就是其中的一种。
Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和 用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。
一.SpringBoot整合Security
1.基本配置
首先导入依赖,SpringSecurity依赖于Spring框架,与之进行对比的是独立存在的shiro框架
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
配置文件如下:只需要改端口号,另外的配置可以后面用到了再加
#设置端口号
server.port=8065
#SpringSecurity配置(更改默认的登录拦截页面的账号与密码)
spring.security.user.name=javaboy
spring.security.user.password=123
#设置静态资源的路径
spring.web.resources.static-locations=classpath:/static/
2.认证与授权
这里给上核心代码:下面将介绍这些代码的作用
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder(){
//设置密码不加密,security在使用自定义页面时,密码默认也是需要加密的
return NoOpPasswordEncoder.getInstance(); //
}
@Override
//认证,用户名,密码,权限
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
//如果需要配置多个用户,可以用and相连 .and().withUser...........
auth.inMemoryAuthentication()
.withUser("javaboy").password("123").roles("admin")
.and()
.withUser("xt").password("123").roles("user");
}
@Override
public void configure(WebSecurity webSecurity) throws Exception{
//配置自定义的登录界面的资源,需要配置文件配置静态资源路径
webSecurity.ignoring().antMatchers("/js/**","/css/**","/images/**");
}
//配置自定义的登录页面
@Override
public void configure(HttpSecurity httpSecurity) throws Exception{
httpSecurity.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")//配置权限
.antMatchers("/user/**").hasRole("user") //配置权限
.anyRequest().authenticated();
httpSecurity.formLogin()
.loginPage("/login.html") //配置登录时的登录页面
.loginProcessingUrl("/login") //配置登录时使用的登录接口
.usernameParameter("username")
.passwordParameter("password")
// .successForwardUrl("/index")
//登录成功的处理
.successHandler((httpServletRequest, httpServletResponse, authentication) -> {
Object principal = authentication.getPrincipal();
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
//压面上展示principal的所有内容
out.write(new ObjectMapper().writeValueAsString(principal));
out.write("123456");
out.flush();
out.close();
})
.permitAll() //允许
.and()
.csrf().disable();
// .exceptionHandling()
// .authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {
// httpServletResponse.setContentType("application/json;charset=utf-8");
// PrintWriter writer = httpServletResponse.getWriter();
// writer.write("尚未登录!");
// writer.flush();
// writer.close();
// });
}
/**
* 角色继承,admin有user的所有功能
*/
@Bean
RoleHierarchy roleHierarchy(){
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_admin > ROLE_user");
return hierarchy;
}
}
(1)认证
其中,认证的代码如下,security在不使用自定义登陆界面时,访问任何路径都会被拦截,并跳转到security的自带的登陆界面,且密码会自动加密并在控制台输出,或者可在配置文件配置用户名与密码。在使用自定义登陆时,默认也是需要将密码进行加密,并且内置了多种加密方式
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
//在内存中定义,也可以在jdbc中去拿....
//Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
//要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
//spring security 官方推荐的是使用bcrypt加密方式。
//可以配置密码不加密
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("qingfeng").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2", "vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2", "vip3")
.and()
.withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2");
}
当然,也可以不使用加密,只需要重写passwordEncoder()方法,并将其作为bean组件注入即可
@Bean
PasswordEncoder passwordEncoder(){
//设置密码不加密,security在使用自定义页面时,密码默认也是需要加密的
return NoOpPasswordEncoder.getInstance(); //
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
//如果需要配置多个用户,可以用and相连 .and().withUser...........
auth.inMemoryAuthentication()
.withUser("java").password("123").roles("admin")
.and()
.withUser("xt").password("123").roles("user");
}
在自定义登陆页面时,可以配置指定的自定义登陆界面所需要的各种资源
@Override
public void configure(WebSecurity webSecurity) throws Exception{
//配置自定义的登录界面的资源
webSecurity.ignoring().antMatchers("/js/**","/css/**","/images/**");
}
(2)授权
配置授权时,使用HttpSecurity,需要配置的首先是授权的url和其所需要的权限,并可以设置任何请求都被拦截监控
可以用 .antMatchers("/").permitAll 来设置登陆页面不需要被验证,也就是根页面
//配置自定义的登录页面
@Override
public void configure(HttpSecurity httpSecurity) throws Exception{
httpSecurity.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")//配置权限
.antMatchers("/user/**").hasRole("user") //配置权限
//设置任何请求都需要被验证
.anyRequest().authenticated();
httpSecurity.formLogin() //设置为表单登录
.loginPage("/login.html") //配置登录时的登录页面
.loginProcessingUrl("/login") //配置登录时使用的登录接口
.successForwardUrl("/index") //配置登陆成功跳转的页面
.failureForwardUrl("/error") //设置登陆失败跳转的页面
.permitAll() //设置允许所有的这些跳转
.and()
.csrf().disable(); //跨域请求伪造
}
其中,从 Spring Security4 开始 CSRF 防护默认开启。默认会拦截请求。进行 CSRF 处理。CSRF 为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf 值为 token(token 在服务端产生)的内容,如果token 和服务端的 token 匹配成功,则正常访问。
这里的匹配规则我们采用了 Ant 风格的路径匹配符,Ant 风格的路径匹配符在 Spring 家族中使用非常广泛,它的匹配规则也非常简单:
| 通配符 | 含义 |
|---|---|
| ** | 匹配多层路径 |
| * | 匹配一层路径 |
| ? | 匹配任意单个字符 |
上面配置的含义是:
- 如果请求路径满足 /admin/** 格式,则用户需要具备 admin 角色。
- 如果请求路径满足 /user/** 格式,则用户需要具备 user 角色。
- 剩余的其他格式的请求路径,只需要认证(登录)后就可以访问。
注意代码中配置的三条规则的顺序非常重要,和 Shiro 类似,Spring Security 在匹配的时候也是按照从上往下的顺序来匹配,一旦匹配到了就不继续匹配了,所以拦截规则的顺序不能写错。
还可以通过重写 WebSecurityConfigurerAdapter 中的 userDetailsService 方法来提供一个 UserDetailService 实例进而配置多个用户:
@Bean
protected UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("student").password("123").roles("admin").build());
manager.createUser(User.withUsername("xiaoming").password("123").roles("user").build());
return manager;
}
具体的复杂认证如下,可以使用successHandler与failureHandler来处理登陆时的失败与成功,
注意:默认情况下,所有未登陆的访问请求都将被拦截,并将跳转到登陆页面
httpSecurity.formLogin()
.loginPage("/login.html") //配置登录时的登录页面
.loginProcessingUrl("/login") //配置登录时使用的登录接口
.usernameParameter("username") //配置用户名的参数
.passwordParameter("password") //配置登陆密码,要与表单中的name一致
.successHandler((httpServletRequest, httpServletResponse, authentication) -> {
Object principal = authentication.getPrincipal();
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write(new ObjectMapper().writeValueAsString(principal));
out.write("123456");
out.flush();
out.close();
})
.permitAll()
.and()
.csrf().disable();
// .exceptionHandling()
// .authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {
// httpServletResponse.setContentType("application/json;charset=utf-8");
// PrintWriter writer = httpServletResponse.getWriter();
// writer.write("尚未登录!");
// writer.flush();
// writer.close();
// });
非前后端分离下:
登录成功回调:
- defaultSuccessUrl 有一个重载的方法,我们先说一个参数的 defaultSuccessUrl 方法。如果我们在 defaultSuccessUrl 中指定登录成功的跳转页面为 /index,此时分两种情况,如果你是直接在浏览器中输入的登录地址,登录成功后,就直接跳转到 /index,如果你是在浏览器中输入了其他地址,例如 http://localhost:8080/hello,结果因为没有登录,又重定向到登录页面,此时登录成功后,就不会来到 /index ,而是来到 /hello 页面。
- defaultSuccessUrl 还有一个重载的方法,第二个参数如果不设置默认为 false,也就是我们上面的的情况,如果手动设置第二个参数为 true,则 defaultSuccessUrl 的效果和 successForwardUrl 一致。
- successForwardUrl 表示不管你是从哪里来的,登录后一律跳转到 successForwardUrl 指定的地址。例如 successForwardUrl 指定的地址为 /index ,你在浏览器地址栏输入 http://localhost:8080/hello,结果因为没有登录,重定向到登录页面,当你登录成功之后,就会服务端跳转到 /index 页面;或者你直接就在浏览器输入了登录页面地址,登录成功后也是来到 /index。
登录失败回调:
与登录成功相似,登录失败也是有两个方法:
- failureForwardUrl
- failureUrl
这两个方法在设置的时候也是设置一个即可。failureForwardUrl 是登录失败之后会发生服务端跳转,failureUrl 则在登录失败之后,会发生重定向。
(3)登录处理(成功/失败/注销):
【1】登录成功:successHandler
该方法有三个参数:
-
req:相当于HttpServletRequest、
-
res:相当于HttpServletRespose
-
authentication:这里保存了我们登录后的用户信息
通常进行如下配置:
principal:用户信息,包括用户名,密码所持有权限,账户是否过期,锁定等配置
PrintWriter:获取页面的输出对象,可以直接将信息输出到页面上,需要刷掉缓存与关闭
.successHandler((httpServletRequest, httpServletResponse, authentication) -> {
//获取用户信息
Object principal = authentication.getPrincipal();
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write(new ObjectMapper().writeValueAsString(principal));
out.write("123456");
out.flush();
out.close();
})
【2】登录失败
该方法有三个参数:
req:相当与HttpServletRequest
res:相当与HttpServletRespose
e:这里保存了我们登录失败的原因
其中,异常种类有如下几种:
-
LockedException 账户锁定
-
CredentialsExpiredException 密码过期
-
AccountExpiredException 账户过期
-
DisabledException 账户被禁止
-
BadCredentialsException 用户名或者密码错误
.failureHandler((req, res, e) -> {
res.setContentType("application/json;charset=utf-8");
PrintWriter out = res.getWriter();
out.write(e.getMessage());
out.flush();
out.close();
})
【3】未认证登录处理方法
spring security默认情况下,如果认证不成功,直接重定向到登录页面。 但是项目中,我们有的时候不需要这样,我们需要在前端进行判断 ,然后再决定进行其他的处理,那我们就可以用authenticationEntryPoint这个接口进行自定义了,取消它的默认重定向行为。
该方法有三个参数:
-
req:相当与HttpServletRequest
-
res:相当与HttpServletRespose
-
authException:指的就是我们未认证的exception
.exceptionHandling() //异常处理
//取消重定向,并给出操作
.authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
writer.write("尚未登录!");
writer.flush();
writer.close();
});
【4】登录注销
注销登录的默认接口是 /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 失效,默认可以不用配置,默认就会清除。
注销登陆的处理如下:
.logoutSuccessHandler((req, res, authentication) -> {
res.setContentType("application/json;charset=utf-8");
PrintWriter out = res.getWriter();
out.write("注销成功");
out.flush();
out.close();
})
实战
登录页面,其请求名必须转到login,请求类型为post
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="title">登录</div>
<form action="/login" method="post">
<div class="input">
<label for="name">用户名</label>
<input type="text" name="username" id="name">
</div>
<div class="input">
<label for="pass">密码</label>
<input type="password" name="password" id="pass">
</div>
<div class="button login">
<button type="submit">
<span>登录</span>
</button>
</div>
</form>
</body>
</html>
接口类
@RestController
public class TestController {
@GetMapping("/hello")
public String test(){
return "hello";
}
@PostMapping("/index")
public String index(){
return "index";
}
@GetMapping("/admin/hello")
public String admin() {
return "admin";
}
@GetMapping("/user/hello")
public String user() {
return "user";
}
}
测试如下:
首先:页面在被访问时会自动跳转到自定义登陆页面
在登陆成功后:会出现包括用户名,密码,权限在内的信息
登录用户为java,只有admin权限
可以访问 /admin/hello路径
当有角色权限继承的时候: 也可以访问到/user/hello页面
/**
* 角色继承,admin有user的所有功能
*/
@Bean
RoleHierarchy roleHierarchy(){
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_admin > ROLE_user");
return hierarchy;
}