文章目录
系列资料
最新版 Spring Security5.x 教程#博客
最新版 Spring Security5.x 教程#源码
笔记
spring security 流程图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zGyD8QbP-1621588647483)(https://note.youdao.com/yws/api/personal/file/WEB43ab8e6d7cff0e1d1d48a177991f9e4d?method=download&shareKey=b4086a495e5665741d5ce836ca6cae90)]
配置登录页面
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.permitAll()
.and()
.csrf().disable();
}
解析:
loginPage("/login.html")
配置登录页面指向/static/login.html
(默认);- 实际上它还有一个隐藏的操作,就是登录接口地址也设置成
/login.html
了。换句话说,新的登录页面和登录接口地址都是/login.html
。他们也可以分开,通过.loginPage("/login.html").loginProcessingUrl("/doLogin")
.具体逻辑参见FormLoginConfigurer
;
替换security默认登录
在static
目录增加 login.html
页面,或者 增加 viewController:
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
}
解析:
默认登录逻辑参看 DefaultLoginPageGeneratingFilter
类的逻辑;
修改登录参数名词
默认参数名称为username
,password
。通过以下配置修改前端参数名称:
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.usernameParameter("name")
.passwordParameter("passwd")
.permitAll()
登录事件回调
成功
- defaultSuccessUrl: 默认成功页面。如果有历史跳转则跳转历史
- successForwardUrl: 表示不管你是从哪里来的,登录后一律跳转到 successForwardUrl 指定的地址。
注意:实际操作中,defaultSuccessUrl 和 successForwardUrl 只需要配置一个即可。
失败
注销登录
注销登录的默认接口是 /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 失效,默认可以不用配置,默认就会清除。
前后端分离,json登录
- 自定义登录验证过滤器;
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String verify_code = (String) request.getSession().getAttribute("verify_code");
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
Map<String, String> loginData = new HashMap<>();
try {
loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
} catch (IOException e) {
}finally {
String code = loginData.get("code");
checkCode(response, code, verify_code);
}
String username = loginData.get(getUsernameParameter());
String password = loginData.get(getPasswordParameter());
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} else {
checkCode(response, request.getParameter("code"), verify_code);
return super.attemptAuthentication(request, response);
}
}
public void checkCode(HttpServletResponse resp, String code, String verify_code) {
if (code == null || verify_code == null || "".equals(code) || !verify_code.toLowerCase().equals(code.toLowerCase())) {
//验证码不正确
throw new AuthenticationServiceException("验证码不正确");
}
}
}
- 注入bean;
@Bean
LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
Hr hr = (Hr) authentication.getPrincipal();
hr.setPassword(null);
RespBean ok = RespBean.ok("登录成功!", hr);
String s = new ObjectMapper().writeValueAsString(ok);
out.write(s);
out.flush();
out.close();
}
});
loginFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
RespBean respBean = RespBean.error(exception.getMessage());
if (exception instanceof LockedException) {
respBean.setMsg("账户被锁定,请联系管理员!");
} else if (exception instanceof CredentialsExpiredException) {
respBean.setMsg("密码过期,请联系管理员!");
} else if (exception instanceof AccountExpiredException) {
respBean.setMsg("账户过期,请联系管理员!");
} else if (exception instanceof DisabledException) {
respBean.setMsg("账户被禁用,请联系管理员!");
} else if (exception instanceof BadCredentialsException) {
respBean.setMsg("用户名或者密码输入错误,请重新输入!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
});
loginFilter.setAuthenticationManager(authenticationManagerBean());
loginFilter.setFilterProcessesUrl("/doLogin");
return loginFilter;
}
- 替换默认过滤器:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
...
//省略
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
验证码功能
- 通过接口获取验证码,同时将验证码放入session 中;
- 自定以验证码过滤器(验证输入和session中的是否一致),并将验证码过滤器置于
UsernamePasswordAuthenticationFilter
之前 ;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(captchaAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
角色访问
角色权限拦截
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated()
.and()
...
...
角色权限继承
上级角色一定可以访问下级资源;
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_admin > ROLE_user");
return hierarchy;
}
注意,在配置时,需要给角色手动加上 ROLE_ 前缀。上面的配置表示 ROLE_admin 自动具备 ROLE_user 的权限。
持久化的用户数据
UserDetailService
Spring Security 支持多种不同的数据源,这些不同的数据源最终都将被封装成 UserDetailsService 的实例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mWuw5hk5-1621588647485)(https://note.youdao.com/yws/api/personal/file/WEB638f4f116aaec88dce24e6790a375492?method=download&shareKey=e66b234b4f4246c0f1525fa573b91ba2)]
可以看到,在几个能直接使用的实现类中,除了 InMemoryUserDetailsManager 之外,还有一个 JdbcUserDetailsManager,使用 JdbcUserDetailsManager 可以让我们通过 JDBC 的方式将数据库和 Spring Security 连接起来。
JdbcUserDetailsManager
JdbcUserDetailsManager 自己提供了一个数据库模型,这个数据库模型保存在org/springframework/security/core/userdetails/jdbc/users.ddl
;
解析:
- users 表中保存用户的基本信息,包括用户名、用户密码以及账户是否可用。
- authorities 中保存了用户的角色。
- authorities 和 users 通过 username 关联起来。
注入一个userDetailService 替换掉默认的 InMemoryUserDetailsManager
:
@Autowired
DataSource dataSource;
@Override
@Bean
protected UserDetailsService userDetailsService() {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
manager.setDataSource(dataSource);
if (!manager.userExists("u0")) {
manager.createUser(User.withUsername("u0").password("123").roles("admin").build());
}
if (!manager.userExists("u1")) {
manager.createUser(User.withUsername("u1").password("123").roles("user").build());
}
return manager;
}
记住用户自动登录
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.rememberMe()
.and()
.csrf().disable();
}