1 为SpringBoot添加Security支持
Security作为Spring的官方安全框架,自然为SpringBoot提供了起步依赖(Starter),有了起步依赖,我们只要添加少量的Java配置,就可以把Security集成到SpringBoot项目中。
1.1 添加 spring-boot-starter-security 依赖
代码语言:javascript复制 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
添加上述依赖后再启动springboot,项目即得到security的保护
默认的登录用户名是“user”,默认密码在启动时输出在控制台中。
这些默认信息被定义在了源码 “org.springframework.boot.autoconfigure.security.SecurityProperties” 中。
1.2 自定义用户验证和授权
要自定义用户的验证和授权需要重写UserDetails接口和UserDetailsService接口,并把UserDetailsService的实现类注册到Security配置中。
(1)实现UserDetails
代码语言:javascript复制public class UserDetailsImpl implements UserDetails {
public UserDetailsImpl(User user, List<Role> roles) {
this.username = user.getUsername();
this.password = user.getPassword();
this.status = user.getStatus();
if(roles!=null) {
this.authorities = roles.stream()
.map(r->new SimpleGrantedAuthority(r.getName()))
.collect(Collectors.toList());
}
}
private String username;
private String password;
private List<GrantedAuthority> authorities=new ArrayList<GrantedAuthority>();
private int status;
//省略属性getter、setter
}
(2)实现UserDetailsService
代码语言:javascript复制@Service("userDetailsService") //托管到Spring容器Seucrity自动识别
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userDb;
@Autowired
private RoleMapper roleDb;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDb.selectOne(new QueryWrapper<User>().eq("username", username));
List<Role> roles = roleDb.findRolesByUserId(user.getId());
return new UserDetailsImpl(user, roles);
}
}
Security框架在重写验证授权时,必须指定密码加密方式,即使密码不做散列加密也需要配置一个空的加密器。
Security的配置信息可以配置在继承了“WebSecurityConfigurerAdapter”父类的配置类中,如下所示。
代码语言:javascript复制@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//必须配置密码加密方式
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();//这里不使用密码加密,配置空的加密器
}
}
1.3 自定义登录页面
重写 WebSecurityConfigurerAdapter 的 config(HttpSecurity http) 方法,可以实现Security的请求配置。
代码语言:javascript复制@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
......
//配置Security细节
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //配置登录
.loginPage("/login")
.loginProcessingUrl("/checklogin")
.defaultSuccessUrl("/index")
.and().logout() //配置注销
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout") //注销后跳转的路径
.and().csrf().disable(); //禁用csrf令牌
}
}
1.4 基于URL请求拦截
config(HttpSecurity http) 方法同样可以配置基于URL的请求拦截。
代码语言:javascript复制@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
......
//配置Security细节
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasAuthority("管理员") // admin目录的所有请求需要管理员权限
.anyRequest().authenticated() // 其它请求需要登录
.and().formLogin()
.loginPage("/login")
.loginProcessingUrl("/checklogin")
.defaultSuccessUrl("/index")
.permitAll() //由于上面配置了所有页面都需要登录,这里必须允许login/logout/index直接访问
.and().logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.and().csrf().disable();
}
}
1.5 基于方法注解的请求拦截
基于URL的请求拦截可能不够精确,尤其不能满足 RESTful API的需求,更合理的可能时基于方法进行注解授权。
(1)允许使用注解前置处理权限
代码语言:javascript复制@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) //允许使用注解前置处理权限
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
......
}
(2)在方法中添加注解@PreAuthorize,指定权限
代码语言:javascript复制@Controller
@RequestMapping("/admin")
@PreAuthorize("hasAuthority('管理员')")
public class AdminController {
@RequestMapping(value={"/","/index"})
public String index(){
return "admin-index";
}
}
2 前后端分离项目的Security配置
对于前后端分离项目而言,后端不再返回页面,更不控制页面跳转,只返回JSON数据。与同步请求中的各种处理后的成功与失败跳转不同,Security需要针对异步请求提供成功或失败后的处理程序(Handler)。
2.1 Security的几种登录成功/失败处理程序:
前后端分离项目需要后端返回JSON数据而非页面,因此需要重写Security的几个处理程序:
(1)处理登录成功
http.formLogin().successHandler((req,resp,authentication)->{ }) 取代 http.formLogin().defaultSuccessUrl(String),设置登录成功后的处理
(2)处理登录失败
http.formLogin().failureHandler((req,resp,authException)->{ ... }) 取代 http.formLogin().failureUrl(String),设置登录失败后的处理
修改配置,实现登录成功(或失败)后使用JSON返回数据
(3)处理匿名(未登录)访问和权限不足请求
用户未登录时访问授权页面,Security会默认重定向到登录页,页面跳转不适用于前后端分离,因此需要授权异常机制。
http.exceptionHandling()
.authenticationEntryPoint((req,resp,authException)->{ ... })
.accessDeniedHandler((req,resp,denyEx)->{ ... })
(4)处理注销成功
http.formLogin().logoutSuccessHandler((req,resp,authentication)->{ }) 取代 http.formLogin().logoutSuccessUrl(url)
2.2 示例:
前后端分离项目的Security配置大致如下:
代码语言:javascript复制 //配置Security细节
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated() //任意请求不允许匿名访问
.and().formLogin()
.loginPage("/login") //不配置loginPage会生成默认登录页
.loginProcessingUrl("/login") //登录处理URL
.successHandler((req,resp,auth)->{ //登录成功处理:返回登录用户信息
resp.setContentType("application/json;charset=utf-8");
String json = new ObjectMapper().writeValueAsString(auth.getPrincipal());
resp.getWriter().print(json);
})
.failureHandler((req,resp,authEx)->{ //登录失败处理:返回401状态码
resp.setContentType("application/json;charset=utf-8");
resp.setStatus(401);
String json = "{"message":"" authEx.getMessage() ""}";
resp.getWriter().print(json);
})
.permitAll() //允许上述路径被匿名访问
.and().logout()
.logoutUrl("/logout")
.logoutSuccessHandler((req,resp,auth)->{//注销成功后处理
resp.setContentType("application/json;charset=utf-8");
String json = "{"message":"注销成功"}";
resp.getWriter().print(json);
})
.and().csrf().disable()
.exceptionHandling() //自定义一些异常处理
//许匿名访问时的处理,返回状态码401(等同于GET请求/login)
.authenticationEntryPoint((req,resp,authException)->{
resp.setContentType("application/json;charset=utf-8");
resp.setStatus(401);
String json = "{"message":"无法匿名访问,请先登录"}";
resp.getWriter().print(json);
})
//权限不足的访问处理,返回状态码403
.accessDeniedHandler((req,resp,denyEx)->{
resp.setContentType("application/json;charset=utf-8");
resp.setStatus(403);
String json = "{"message":"" denyEx.getMessage() ""}";
resp.getWriter().print(json);
});
}