Spring Security登录的认证和授权
一、Spring Security简介
Spring Security是一个专注于为Java应用程序提供身份认证和授权的框架,是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
spring security 的核心功能主要包括:
- 认证 (你是谁)
- 授权 (你能干什么)
- 攻击防护 (防止伪造身份)
二、入门实现登录的认证和授权
具体代码可在码云拷贝:项目源码
【在pom.xml文件导入依赖】
org.springframework.bootspring-boot-starter-security
导入security依赖后,会立刻对项目产生影响,再次访问项目的任何页面都需要登录,登录账号默认为:user,在日志中给出随机密码
【User实体类实现UserDetails接口】
在用户的实体类User中继承UserDetails接口,并实现下面五个方法:
// true: 账号未过期.@Overridepublic boolean isAccountNonExpired() { return true;}
// true: 账号未锁定.@Overridepublic boolean isAccountNonLocked() { return true;}
// true: 凭证未过期.@Overridepublic boolean isCredentialsNonExpired() { return true;}
// true: 账号可用.@Overridepublic boolean isEnabled() { return true;}
// 获取用户权限@Overridepublic Collection getAuthorities() { List list = new ArrayList(); list.add(new GrantedAuthority() { @Override public String getAuthority() { // type表示自定义用户的身份, 1是管理员, 其余是普通用户 switch (type) { case 1: return "ADMIN"; default: return "USER"; } } }); return list;}
【在UserService中实现UserDetailsService方法】
在用户的业务逻辑中实现UserDetailsService方法,并实现loadUserByUsername方法:
@Servicepublic class UserService implements UserDetailsService { @Autowired private UserMapper userMapper; public User findUserByName(String username) { return userMapper.selectByName(username); } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return this.findUserByName(username); }}
【创建SecurityConfig配置类, 实现过滤】
配置类继承WebSecurityConfigurerAdapter类,并重写 configuer 方法,需要重写三种重载形式的configure
- AuthenticationManager: 认证的核心接口.
- AuthenticationManagerBuilder: 用于构建AuthenticationManager对象的工具.
- ProviderManager: AuthenticationManager接口的默认实现类.
- AuthenticationProvider: ProviderManager将持有一组AuthenticationProvider, 每个AuthenticationProvider负责一种认证.
- 委托模式: ProviderManager将认证委托给AuthenticationProvider
- Authentication: 用于封装认证信息(账号密码等)的接口, 不同的实现类代表不同类型的认证信息.
@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Override public void configure(WebSecurity web) throws Exception { // 忽略静态资源的访问 web.ignoring().antMatchers("/resource/**"); } // AuthenticationManager: 认证的核心接口. // AuthenticationManagerBuilder: 用于构建AuthenticationManager对象的工具. // ProviderManager: AuthenticationManager接口的默认实现类. @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 1. 内置的认证规则 // auth.userDetailsService(userService).passwordEncoder(new Pbkdf2PasswordEncoder("12345")); // 2. 自定义认证规则 // AuthenticationProvider: ProviderManager将持有一组AuthenticationProvider, 每个AuthenticationProvider负责一种认证. // 委托模式: ProviderManager将认证委托给AuthenticationProvider. auth.authenticationProvider(new AuthenticationProvider() { // Authentication: 用于封装认证信息(账号密码等)的接口, 不同的实现类代表不同类型的认证信息. @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getName(); String password = (String) authentication.getCredentials(); User user = userService.findUserByName(username); if(user == null) { throw new UsernameNotFoundException("账号不存在!"); } password = CommunityUtil.md5(password + user.getSalt()); if(!password.equals(user.getPassword())) { throw new BadCredentialsException("密码不正确!"); } // principal: 认证的主要信息; credentials: 证书; authorities: 权限 return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities()); } // 当前的AuthenticationProvider支持哪种类型的认证. @Override public boolean supports(Class aClass) { // UsernamePasswordAuthenticationToken: Authentication接口的常用的实现类(账号密码). return UsernamePasswordAuthenticationToken.class.equals(aClass); } }); } @Override protected void configure(HttpSecurity http) throws Exception { // 登录相关配置 http.formLogin() .loginPage("/loginpage")// 登录时的页面 .loginProcessingUrl("/login")// 发送登录请求的路径 .successHandler(new AuthenticationSuccessHandler() { // 登录成功的处理 @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.sendRedirect(request.getContextPath() + "/index"); } }) .failureHandler(new AuthenticationFailureHandler() { // 登陆失败的操作 @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { request.setAttribute("error", e.getMessage()); request.getRequestDispatcher("/loginpage").forward(request, response); } }); // 退出相关配置 http.logout() .logoutUrl("/logout")// 退出登录的路径 .logoutSuccessHandler(new LogoutSuccessHandler() { // 退出成功的处理 @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.sendRedirect(request.getContextPath() + "/index"); } }); // 授权配置 http.authorizeRequests() .antMatchers("/letter").hasAnyAuthority("USER", "ADMIN")// 为指定页面配置指定权限 .antMatchers("/admin").hasAnyAuthority("ADMIN") .and().exceptionHandling().accessDeniedPage("/denied");// 访问失败后要去的路径 // 增加Filter, 处理验证码 http.addFilterBefore(new Filter() { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; if(request.getServletPath().equals("/login")) { // 如果是登录页面, 才处理验证码 String verifyCode = request.getParameter("verifyCode"); if(verifyCode == null || !verifyCode.equals("12345")) { // 验证码不正确 request.setAttribute("error", "验证码错误!"); request.getRequestDispatcher("/loginpage").forward(request, response); return; } } // 让请求继续向下执行 filterChain.doFilter(request, response); } }, UsernamePasswordAuthenticationFilter.class); // 记住我 http.rememberMe() .tokenRepository(new InMemoryTokenRepositoryImpl()) .tokenValiditySeconds(3600 * 24) .userDetailsService(userService); }}
认证成功后,结果会通过SecurityContextHolder存入SecurityContext中.
@RequestMapping(path = "/index", method = RequestMethod.GET)public String getIndexPage(Model model) { // 认证成功后,结果会通过SecurityContextHolder存入SecurityContext中. Object obj = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if(obj instanceof User) { model.addAttribute("loginUser", obj); } return "/index";}
【CSRF配置】
csrf攻击:当用户提交表单数据时,可能会收到csrf的攻击,获取表单中的登录信息,从而盗取用户信息。
Spring Security默认对普通请求的表单的隐藏域中加上csrf的检验序列(令牌),而csrf病毒攻击时不持有令牌将会被拒绝访问
但对于AJAX请求,Spring Security没有默认的检验序列,所以需要我们自动配置。
对AJAX请求配置检验序列:
在index页面手动配置生成CSRF令牌:
在js文件的发布内容的事件中将CSRF令牌设置到请求的消息头中:
授权时加上http.csrf().disable();可以废弃Spring Security对csrf攻击的保护。
注:
在SecurityConfig类的配置中,若没有使用Filter过滤器认证用户信息,只是授权用户无法实现过滤功能,需要在Interceptor中拦截用户并构建用户的认证信息,然后手动地存入SecurityContext中,以便Security进行授权。
在请求结束时记得清理用户的认证信息
三、thymeleaf + spring security
权限管理可以分为两个层面:
- 在服务器层面过滤掉没有相应权限的用户,不让其访问对应的功能
- 在前端页面中友好的向不同身份权限的用户展示不同的功能
这里,thymeleaf 内置的标签支持 spring security,但若想使用其功能还需要导入相关包,选择对应父pom中的版本。
!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5 -->org.thymeleaf.extrasthymeleaf-extras-springsecurity5
在页面顶端声明命名空间,在github文档末尾可找到: