一、spring security讲解:
Spring Security是spring提供的一种基于 Spring AOP 和 Servlet 过滤器的安全框架,其提供了对网页端请求级和方法调用级的处理身份认证(用户身份认证)和授权(登录的用户拥有什么权限)。Spring Security的核心是一组过滤器链,在springboot中引入依赖即默认对多有接口启动了安全管理,其中最核心的就是用来认证用户的身份的Basic Authentication Filter 。
但由于在传统MVC框架下整合 Spring Security需要大量配置,比较麻烦,所以大部分人偏向于使用功能没它强大的Shiro 。然而springboot的出现使其又回到人们的视野,springboot对Spring Security 提供了 自动化配置,达到了不用配置即可使用 Spring Security。
二、使用(spring boot 基础上,springboot2.2.5)
1、引入Spring Security依赖(只要加入依赖,项目的所有接口都会被自动保护起来,即默认都要登录认证):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、我们写一个测试的controller接口
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
}
在没加入依赖前,项目启动即可访问该接口。加入Spring Security依赖后访问该接口(返回 302 响应码),默认重定向到 localhost:8080/login 的登录页地址(该登录页默认含有一个form表单,表单含有用户名属性:username,密码属性:password,表单提交的url为 /login),输入用户名 user、密码(服务启动日志查看)登录后才会重新跳到访问的 /hello 接口地址。
3、默认登录账号说明
上面讲到默认登录的账号为user,我们也可以自己设置账号信息(三种方式)。
- 在配置文件添加用户、密码信息
- 编写代码继承 WebSecurityConfigurerAdapter 类,在该类中配置用户信息,将会把信息存到内存
- 用户信息配置在数据库,从数据库获取(真正生产上使用的方式)
(1)配置文件
spring.security.user.name=testname
spring.security.user.password=testpass
#springboot 1.xx 使用如下配置
#security.user.name=testname
#security.user.password=testpass
(2)代码继承 WebSecurityConfigurerAdapter 类
@Configuration
//利用@EnableWebSecurity 注解继承 WebSecurityConfigurerAdapter的类,这样就构成了 Spring Security 的配置
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//下面这两行配置表示在内存中配置了两个用户,这里两个用户的密码到是123456,在springboot1.xx版本密码设置不用加{noop}
auth.inMemoryAuthentication()
// testadmin同时具有 ADMIN,USER权限,testuser只有USER权限(大写)
.withUser("testadmin").password("{noop}123456").roles("ADMIN","USER")
.and()
.withUser("testuser").password("{noop}123456").roles("USER");
}
}
(3)数据库的配置等后面讲
4、在SecurityConfig类中添加不同用户能访问的权限资源(特定资源只能由特定角色访问)
因为上面用户配置中,testadmin拥有ADMIN、USER两个权限,所以可以访问任何资源;testuser只有USER权限,只能访问/user/** 对应的接口
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() //开启登录配置
//表示/user开头的接口拥有USER权限才能访问
.antMatchers("/user/**").hasRole("USER")
//表示/admin开头的接口拥有ADMIN权限才能访问
.antMatchers("/admin/**").hasRole("ADMIN")
//表示剩余的其他接口,登录之后就能访问,不涉及权限问题
.anyRequest().authenticated()
.and()
//指定支持基于表单的身份验证,当需要用户登录时候,转到默认的登录页面。可以通过loginPage(String)指定自定义的登录页面
.formLogin()
.and()
//开启http basic
.httpBasic();
}
5、在SecurityConfig类中添加放行静态资源
下面定义 /filter 接口,/js/**、/css/**的静态资源不需要登录即可访问
@Override
public void configure(WebSecurity web) throws Exception {
//某一个请求地址不需要拦截的话,直接过滤掉该地址,即该地址不走 Spring Security 过滤器链,主要一些静态资源
web.ignoring().antMatchers("/filter", "/js/**", "/css/**");
}
6、最终 SecurityConfig类
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("testadmin").password("{noop}123456").roles("ADMIN","USER")
.and()
.withUser("testuser").password("{noop}123456").roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/filter", "/js/**", "/css/**");
}
}
7、controller接口
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/hello")
public String userHasRole(){
return "user";
}
}
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/hello")
public String adminHasRole(){
return "admin";
}
}
8、通过 SecurityContextHolder 获取登录的用户信息
在接口中,可以通过SecurityContextHolder 类来获取当前登录并调用该接口的用户信息
@GetMapping("/hello")
public String hello(){
String username = "";
Object principl = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(principl instanceof UserDetails) {
username = ((UserDetails)principl).getUsername();
}else {
username = principl.toString();
}
System.out.println("当前登录用户为:" + username);
return "hello";
}
9、另外我们还可以直接在controller的接口上定义用户需要的角色权限(注意类上@EnableGlobalMethodSecurity(prePostEnable = true) 注解才能支持)
角色设置一定要ROLE_前缀,这是security定义的,可以在源码(RoleVote.java)看到。
- @PreAuthorize :接口方法调用前做认证
- @PostAuthorize :接口方法调用后做认证
- @PreFilter :主要用于接口调用前对List参数做验证
- @PostFilter:主要用于接口返回的List做验证
@RestController
@RequestMapping("/admin")
@EnableGlobalMethodSecurity(prePostEnable = true)
public class AdminController {
@GetMapping("/hello")
@PreAuthorize("hasRole('ROLE_ADMIN')") //表示访问该接口需要ADMIN角色,一定要ROLE_前缀
public String adminHasRole(){
return "admin";
}
@GetMapping("/hello1")
//可以加多个角色,另外还要很多hasxxx方法等
@PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')")
public String adminHasRole1(){
return "admin";
}
@GetMapping("/hello2")
//可以对请求参数做处理,传入参数id要大于1,且name为当前登录用户,不符合则抛异常
@PreAuthorize("#id>1 and principal.username.equals(#name)")
public String adminHasRole2(Integer id,String name){
return "admin";
}
@GetMapping("/hello3")
//返回的字符串为122,不是则抛异常
@PostAuthorize("hasRole('ROLE_ADMIN') and returnObject.equals('122')")
public String adminHasRole3(Integer id){
return "122";
}
@GetMapping("/hello4")
//传入的集合个数是否为2的倍数,返回的集合个数是否为4的倍数
@PreFilter("filterObject%2==0")
@PostFilter("filterObject%4==0")
public String adminHasRole4(List<String> list){
return list;
}
}
10、附加对 SecurityConfig 类中配置权限规则的方法进行更多说明
- permitAll:永远为true,即都能访问
- denyAll:永远为false,不对外访问
- anonymous:用户为匿名时可访问
- rememberMe:用户为rememberMe能访问
- authenticated:不是匿名也不是rememberMe才能访问
- hasRole(role):用户有指定角色权限才能访问
- hasAnyRole([role1,role2]):有任一角色权限能访问
- hasAuthority(authority):有指定权限时能访问,如 hasAuthority("read"),基本通hasRole
- hasAnyAuthority([authority1,authority2]):有任意一个指定权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //开启登录配置
// /a,/b接口不需要任何身份验证,permitAll表示放行
.antMatchers("/a","/b").permitAll()
//表示/user/*请求中的GET请求才会到这里,同时可以通过access指定多个权限条件
.antMatchers(HttpMethod.GET,"/user/*").access("hasRole('ADMIN') and hasAnyAuthority('xx')")
//表示剩余的其他接口,登录之后就能访问,authenticated表示需要登录认证,放在其他权限设置后
.anyRequest().authenticated()
//每一个放行/拦截描述之后加and才能继续描述
.and()
//指定支持基于表单的身份验证。
.formLogin()
//定义登录页面,未登录时,访问一个需要登录之后才能访问的接口,会自动重定向到该请求
.loginPage("/login.html")
//登录处理接口,默认登录页面表单提交的接口为/login,我们如果自己写login页面,则这里要修改为login页面表单登录对应的url,但url接口不用我们提供
.loginProcessingUrl("/doLogin")
//定义登录时用户名的 key,默认登录页面表单的属性为 username
.usernameParameter("name")
//定义登录时用户密码的 key,默认登录页面表单的属性为 password
.passwordParameter("pass")
// successHandler 方法中,配置登录成功的回调
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//httpServletResponse.setContentType("application/json;charset=utf-8");
//httpServletResponse.getWriter().write(objectMapper.writeValueAsString(authentication)); //返回json给调用方
System.out.println("success");
}
})
//登录失败回调
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
System.out.println("fail");
}
})
//和上面表示的表单登录相关的接口统统都直接通过
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
//logoutSuccessHandler 中则配置注销成功的回调
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("logout success");
}
})
.permitAll()
.and()
.httpBasic()
.and()
//使用WebSecurityConfigurerAdapter时,默认启用CSRF,这里关闭
.csrf().disable();
//配置基于x509的认证
http.x509();
}