2021-05-10

Spring Security 入门篇

本文是一个笔记系列,目标是完成一个基于角色的权限访问控制系统(RBAC),有基本的用户、角色、权限管理,重点在Spring Security的各种配置。万丈高楼平地起,接下来,一步一步,由浅入深,希望给一起学习的小伙伴一个参考。

1.  Hello Security

按照惯例,先写个Hello World

首先,引入依赖

1 <dependency>2  <groupId>org.springframework.boot</groupId>3  <artifactId>spring-boot-starter-security</artifactId>4 </dependency>

先来理清楚"认证"和"授权"两个概念。认证就是告诉我你是谁,授权就是你可以做什么。结合实际项目通俗地来讲,认证就是登录,授权就是访问资源。故而,我们需要先有用户和资源,先简单地定义几个内存用户和资源吧,为此需要在WebSecurtiyConfigurerAdapter中进行配置。

WebSecurityConfig.java

 1 package com.example.demo.config; 2  3 import org.springframework.context.annotation.Bean; 4 import org.springframework.context.annotation.Configuration; 5 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 6 import org.springframework.security.config.annotation.web.builders.HttpSecurity; 7 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 8 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 9 import org.springframework.security.crypto.password.PasswordEncoder;10 11 @Configuration12 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {13 14  @Override15  protected void configure(AuthenticationManagerBuilder auth) throws Exception {16   auth.inMemoryAuthentication()17     .withUser("zhangsan").password(passwordEncoder().encode("123456")).roles("user")18     .and()19     .withUser("admin").password(passwordEncoder().encode("123456")).roles("admin")20     .and()21     .passwordEncoder(passwordEncoder());22  }23 24  @Override25  protected void configure(HttpSecurity http) throws Exception {26   http.formLogin()27 //    .loginPage("/login.html")28     .loginProcessingUrl("/login")29     .usernameParameter("username")30     .passwordParameter("password")31     .defaultSuccessUrl("/")32    .and()33     .authorizeRequests()34     .antMatchers("/login.html", "/login").permitAll()35     .antMatchers("/hello/sayHello").hasAnyAuthority("ROLE_user", "ROLE_admin")36     .antMatchers("/hello/sayHi").hasAnyRole("admin")37    .anyRequest().authenticated();38  }39 40  @Bean41  public PasswordEncoder passwordEncoder() {42   return new BCryptPasswordEncoder();43  }44 }

HelloController.java

 1 package com.example.demo.controller; 2  3 import org.springframework.web.bind.annotation.GetMapping; 4 import org.springframework.web.bind.annotation.RequestMapping; 5 import org.springframework.web.bind.annotation.RestController; 6  7 @RestController 8 @RequestMapping("/hello") 9 public class HelloController {10 11  @GetMapping("/sayHello")12  public String sayHello() {13   return "hello";14  }15 16  @GetMapping("/sayHi")17  public String sayHi() {18   return "hi";19  }20 21 }

项目结构

定义了两个用户zhangsan和admin,他们的密码都是123456,zhangsan的角色是user可以访问/hello/sayHello,admin的角色是admin可以访问/hello/sayHello和hello/sayHi

2.  认证成功/失败处理

按照刚才的写法,登录成功之后是跳到/页面,失败跳转到登录页。但是,对于前后端分离的项目,我希望它返回json数据,而不是重定向到某个页面

处理用户名和密码登录的过滤器是org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter,既然是过滤器,直接看doFilter方法

不用多说,自定义认证成功处理器 

 1 package com.example.demo.handler; 2  3 import com.faster; 4 import org.springframework.security.core.Authentication; 5 import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; 6 import org.springframework.stereotype.Component; 7  8 import javax.servlet.ServletException; 9 import javax.servlet.http.HttpServletRequest;10 import javax.servlet.http.HttpServletResponse;11 import java.io.IOException;12 13 @Component14 public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {15 16  private static ObjectMapper objectMapper = new ObjectMapper();17 18  @Override19  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {20   response.setContentType("application/json;charset=utf-8");21   response.getWriter().write(objectMapper.writeValueAsString("ok"));22  }23 }

自定义认证失败处理器

 1 package com.example.demo.handler; 2  3 import com.faster; 4 import org.springframework.security.core.AuthenticationException; 5 import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; 6 import org.springframework.stereotype.Component; 7  8 import javax.servlet.ServletException; 9 import javax.servlet.http.HttpServletRequest;10 import javax.servlet.http.HttpServletResponse;11 import java.io.IOException;12 13 @Component14 public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {15 16  private static ObjectMapper objectMapper = new ObjectMapper();17 18  @Override19  public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {20   response.setContentType("application/json;charset=utf-8");21   response.getWriter().write(objectMapper.writeValueAsString("error"));22  }23 }

WebSecurityConfig配置

 1 package com.example.demo.config; 2  3 import com.example.demo.handler.MyAuthenticationFailureHandler; 4 import com.example.demo.handler.MyAuthenticationSuccessHandler; 5 import com.example.demo.handler.MyExpiredSessionStrategy; 6 import org.springframework.beans.factory.annotation.Autowired; 7 import org.springframework.context.annotation.Bean; 8 import org.springframework.context.annotation.Configuration; 9 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;10 import org.springframework.security.config.annotation.web.builders.HttpSecurity;11 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;12 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;13 import org.springframework.security.crypto.password.PasswordEncoder;14 15 @Configuration16 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {17 18  @Autowired19  private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;20  @Autowired21  private MyAuthenticationFailureHandler myAuthenticationFailureHandler;22 23  @Override24  protected void configure(AuthenticationManagerBuilder auth) throws Exception {25   auth.inMemoryAuthentication()26     .withUser("zhangsan").password(passwordEncoder().encode("123456")).roles("user")27     .and()28     .withUser("admin").password(passwordEncoder().encode("123456")).roles("admin")29     .and()30     .passwordEncoder(passwordEncoder());31  }32 33  @Override34  protected void configure(HttpSecurity http) throws Exception {35   http.formLogin()36 //    .loginPage("/login.html")37     .loginProcessingUrl("/login")38     .usernameParameter("username")39     .passwordParameter("password")40 //    .defaultSuccessUrl("/")41     .successHandler(myAuthenticationSuccessHandler)42     .failureHandler(myAuthenticationFailureHandler)43    .and()44     .authorizeRequests()45     .antMatchers("/login.html", "/login").permitAll()46     .antMatchers("/hello/sayHello").hasAnyAuthority("ROLE_user", "ROLE_admin")47     .antMatchers("/hello/sayHi").hasAnyRole("admin")48    .anyRequest().authenticated()49    .and()50     .sessionManagement().sessionFixation().migrateSession()51     .maximumSessions(1).maxSessionsPreventsLogin(false).expiredSessionStrategy(new MyExpiredSessionStrategy());52  }53 54  @Bean55  public PasswordEncoder passwordEncoder() {56   return new BCryptPasswordEncoder();57  }58 }

再多自定义一个Session过期策略,当Session过期或者被踢下线以后的处理逻辑

 1 package com.example.demo.handler; 2  3 import com.faster; 4 import org.springframework.security.web.session.SessionInformationExpiredEvent; 5 import org.springframework.security.web.session.SessionInformationExpiredStrategy; 6  7 import javax.servlet.ServletException; 8 import javax.servlet.http.HttpServletResponse; 9 import java.io.IOException;10 11 public class MyExpiredSessionStrategy implements SessionInformationExpiredStrategy {12 13  private static ObjectMapper objectMapper = new ObjectMapper();14 15  @Override16  public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {17   String msg = "登录超时或已在另一台机器登录,您被迫下线!";18   HttpServletResponse response = event.getResponse();19   response.setContentType("application/json;charset=utf-8");20   response.getWriter().write(objectMapper.writeValueAsString(msg));21  }22 }

3.  从数据库中加载用户及权限

刚才用户是在内存中定义的,这肯定是不行的,下面从数据库中加载用户及其所拥有的权限

最简单的结构是这样的:

为了减少用户的重复授权,引入用户组。将用户加入用户组以后,就自动拥有组所对应的权限。

下面,按照最简单的用户角色权限模型来改造刚才的项目

首先,通过实现UserDetails接口来自定义一个用户信息对象

MyUserDetails.java

 1 package com.example.demo.model; 2  3 import org.springframework.security.core.GrantedAuthority; 4 import org.springframework.security.core.userdetails.UserDetails; 5  6 import java.util.Collection; 7  8 public class MyUserDetails implements UserDetails { 9 10  private String username;11  private String password;12  private boolean enabled;13  private Collection<? extends GrantedAuthority> authorities;14 15  public MyUserDetails(String username, String password, boolean enabled) {16   this.username = username;17   this.password = password;18   this.enabled = enabled;19  }20 21  public void setUsername(String username) {22   this.username = username;23  }24 25  public void setPassword(String password) {26   this.password = password;27  }28 29  public void setEnabled(boolean enabled) {30   this.enabled = enabled;31  }32 33  public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {34   this.authorities = authorities;35  }36 37  @Override38  public Collection<? extends GrantedAuthority> getAuthorities() {39   return authorities;40  }41 42  @Override43  public String getPassword() {44   return password;45  }46 47  @Override48  public String getUsername() {49   return username;50  }51 52  @Override53  public boolean isAccountNonExpired() {54   return true;55  }56 57  @Override58  public boolean isAccountNonLocked() {59   return true;60  }61 62  @Override63  public boolean isCredentialsNonExpired() {64   return true;65  }66 67  @Override68  public boolean isEnabled() {69   return enabled;70  }71 }

有了UserDetails以后,还需要UserDetailsService去加载它,所以自定义一个UserDetailsService

MyUserDetailsService.java

 1 package com.example.demo.service; 2  3 import com.example.demo.entity.*; 4 import com.example.demo.model.MyUserDetails; 5 import com.example.demo.repository.*; 6 import org.springframework.beans.factory.annotation.Autowired; 7 import org.springframework.security.core.GrantedAuthority; 8 import org.springframework.security.core.authority.SimpleGrantedAuthority; 9 import org.springframework.security.core.userdetails.UserDetails;10 import org.springframework.security.core.userdetails.UserDetailsService;11 import org.springframework.security.core.userdetails.UsernameNotFoundException;12 import org.springframework.stereotype.Component;13 14 import java.util.ArrayList;15 import java.util.List;16 import java.util.Optional;17 import java.util.stream.Collectors;18 19 @Component20 public class MyUserDetailsService implements UserDetailsService {21 22  @Autowired23  private SysUserRepository sysUserRepository;24  @Autowired25  private SysRoleRepository sysRoleRepository;26  @Autowired27  private SysUserRoleRelationRepository sysUserRoleRelationRepository;28  @Autowired29  private SysRolePermissionRelationRepository sysRolePermissionRelationRepository;30  @Autowired31  private SysPermissionRepository sysPermissionRepository;32 33  @Override34  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {35   // 查用户36   Optional<SysUser> optionalSysUser = sysUserRepository.findByUsername(username);37   SysUser sysUser = optionalSysUser.orElseThrow(()->new UsernameNotFoundException("用户名" + username + "不存在"));38 39   // 查权限40   List<SysUserRoleRelation> sysUserRoleRelationList = sysUserRoleRelationRepository.findByUserId(sysUser.getId());41   List<Integer> roleIds = sysUserRoleRelationList.stream().map(SysUserRoleRelation::getRoleId).collect(Collectors.toList());42   List<SysRole> sysRoleList = sysRoleRepository.findByIdIn(roleIds);43   List<SysRolePermissionRelation> sysRolePermissionRelationList = sysRolePermissionRelationRepository.findByRoleIdIn(roleIds);44   List<Integer> permissionIds = sysRolePermissionRelationList.stream().map(SysRolePermissionRelation::getPermissionId).collect(Collectors.toList());45   List<SysPermission> sysPermissionList = sysPermissionRepository.findByIdIn(permissionIds);46 47   List<GrantedAuthority> grantedAuthorities = new ArrayList<>(sysPermissionList.size());48   for (SysPermission permission : sysPermissionList) {49    grantedAuthorities.add(new SimpleGrantedAuthority(permission.getUrl()));50   }51   sysRoleList.forEach(role->grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleCode())));52 53   MyUserDetails myUserDetails = new MyUserDetails(sysUser.getUsername(), sysUser.getPassword(), sysUser.isEnabled());54   myUserDetails.setAuthorities(grantedAuthorities);55 56   return myUserDetails;57  }58 }

这里用的JPA,相关的实体类及Repository太多就不一一贴出来了,只代表性的贴一个

SysRole.java 

 1 package com.example.demo.entity; 2  3 import lombok.Data; 4  5 import javax.persistence.*; 6 import java.io.Serializable; 7 import java.time.LocalDateTime; 8  9 @Data10 @Entity11 @Table(name = "sys_role")12 public class SysRole implements Serializable {13 14  @Id15  @GeneratedValue(strategy = GenerationType.AUTO)16  private Integer id;17 18  private String roleName;19 20  private String roleCode;21 22  private String roleDesc;23 24  private LocalDateTime createTime;25 26  private LocalDateTime updateTime;27 }

SysRoleRepository.java

 1 package com.example.demo.repository; 2  3 import com.example.demo.entity.SysRole; 4 import org.springframework.data.jpa.repository.JpaRepository; 5  6 import java.util.List; 7  8 public interface SysRoleRepository extends JpaRepository<SysRole, Integer> { 9 10  List<SysRole> findByIdIn(List<Integer> ids);11 }

application.properties

1 spring.datasource.url=jdbc:mysql://localhost:3306/test2 spring.datasource.username=root3 spring.datasource.password=1234564 spring.datasource.driver-class-name=com.mysql.jdbc.Driver5 6 spring.jpa.database=mysql

最后,也是最重要的是配置WebSecurity

WebSecurityConfig.java 

 1 package com.example.demo.config; 2  3 import com.example.demo.handler.MyAuthenticationFailureHandler; 4 import com.example.demo.handler.MyAuthenticationSuccessHandler; 5 import com.example.demo.handler.MyExpiredSessionStrategy; 6 import com.example.demo.service.MyUserDetailsService; 7 import org.springframework.beans.factory.annotation.Autowired; 8 import org.springframework.context.annotation.Bean; 9 import org.springframework.context.annotation.Configuration;10 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;11 import org.springframework.security.config.annotation.web.builders.HttpSecurity;12 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;13 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;14 import org.springframework.security.crypto.password.PasswordEncoder;15 16 @Configuration17 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {18 19  @Autowired20  private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;21  @Autowired22  private MyAuthenticationFailureHandler myAuthenticationFailureHandler;23  @Autowired24  private MyUserDetailsService myUserDetailsService;25 26  @Override27  protected void configure(AuthenticationManagerBuilder auth) throws Exception {28   auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());29  }30 31  @Override32  protected void configure(HttpSecurity http) throws Exception {33   http.formLogin()34     .loginProcessingUrl("/login")35     .usernameParameter("username")36     .passwordParameter("password")37     .successHandler(myAuthenticationSuccessHandler)38     .failureHandler(myAuthenticationFailureHandler)39    .and()40     .authorizeRequests()41     .antMatchers("/login.html", "/login").permitAll()42     .antMatchers("/hello/sayHello").hasAnyAuthority("ROLE_user", "ROLE_admin")43     .antMatchers("/hello/sayHi").hasAnyRole("admin")44    .anyRequest().authenticated()45    .and()46     .sessionManagement().sessionFixation().migrateSession()47     .maximumSessions(1).maxSessionsPreventsLogin(false).expiredSessionStrategy(new MyExpiredSessionStrategy());48  }49 50  @Bean51  public PasswordEncoder passwordEncoder() {52   return new BCryptPasswordEncoder();53  }54 55 }

改完后的项目结构如下

 

4.  动态加载权限规则配置

鉴权规则就是判断请求的资源是不是在当前用户可访问的资源列表中

那么,首先,定义一个方法来实现这个逻辑

 1 package com.example.demo.service; 2  3 import org.springframework.security.core.Authentication; 4 import org.springframework.security.core.authority.SimpleGrantedAuthority; 5 import org.springframework.security.core.userdetails.UserDetails; 6 import org.springframework.stereotype.Component; 7  8 import javax.servlet.http.HttpServletRequest; 9 10 @Component("myAccessDecisionService")11 public class MyAccessDecisionService {12 13  public boolean hasPermission(HttpServletRequest request, Authentication authentication) {14   Object principal = authentication.getPrincipal();15   if (principal instanceof UserDetails) {16    UserDetails userDetails = (UserDetails) principal;17    SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(request.getRequestURI());18    return userDetails.getAuthorities().contains(simpleGrantedAuthority);19   }20   return false;21  }22 }

然后,在WebSecurityConfig中配置,替换原来写死的匹配规则

 1 package com.example.demo.config; 2  3 import com.example.demo.handler.MyAuthenticationFailureHandler; 4 import com.example.demo.handler.MyAuthenticationSuccessHandler; 5 import com.example.demo.handler.MyExpiredSessionStrategy; 6 import com.example.demo.service.MyUserDetailsService; 7 import org.springframework.beans.factory.annotation.Autowired; 8 import org.springframework.context.annotation.Bean; 9 import org.springframework.context.annotation.Configuration;10 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;11 import org.springframework.security.config.annotation.web.builders.HttpSecurity;12 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;13 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;14 import org.springframework.security.crypto.password.PasswordEncoder;15 16 @Configuration17 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {18 19  @Autowired20  private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;21  @Autowired22  private MyAuthenticationFailureHandler myAuthenticationFailureHandler;23  @Autowired24  private MyUserDetailsService myUserDetailsService;25 26  @Override27  protected void configure(AuthenticationManagerBuilder auth) throws Exception {28   auth.userDetailsService(myUserDetailsService)29     .passwordEncoder(passwordEncoder());30  }31 32  @Override33  protected void configure(HttpSecurity http) throws Exception {34   http.formLogin()35     .loginProcessingUrl("/login")36     .usernameParameter("username")37     .passwordParameter("password")38     .successHandler(myAuthenticationSuccessHandler)39     .failureHandler(myAuthenticationFailureHandler)40    .and()41     .authorizeRequests()42     .antMatchers("/login.html", "/login").permitAll()43     .anyRequest().access("@myAccessDecisionService.hasPermission(request, authentication)")44    .and()45     .sessionManagement().sessionFixation().migrateSession()46     .maximumSessions(1).maxSessionsPreventsLogin(false).expiredSessionStrategy(new MyExpiredSessionStrategy());47  }48 49  @Bean50  public PasswordEncoder passwordEncoder() {51   return new BCryptPasswordEncoder();52  }53 54 }

改造后的项目结构如下

 

关于权限(资源)访问规则,还有一种写法,这种方式是我在网上看到的,就是利用 FilterInvocationSecurityMetadataSource 和 AccessDecisionManager

这里我稍微改了一下,先来创建两个实现类

首先是MyFilterInvocationSecurityMetadataSource.java

 1 package com.example.demo.service; 2  3 import com.example.demo.entity.SysPermission; 4 import com.example.demo.repository.SysPermissionRepository; 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.security.access.ConfigAttribute; 7 import org.springframework.security.access.SecurityConfig; 8 import org.springframework.security.web.FilterInvocation; 9 import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;10 import org.springframework.stereotype.Component;11 import org.springframework.util.AntPathMatcher;12 import org.springframework.util.CollectionUtils;13 14 import java.util.Collection;15 import java.util.List;16 import java.util.stream.Collectors;17 18 @Component19 public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {20 21  private AntPathMatcher pathMatcher = new AntPathMatcher();22 23  @Autowired24  private SysPermissionRepository sysPermissionRepository;25 26  @Override27  public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {28   String requestUrl = ((FilterInvocation) object).getRequestUrl();29 30   // 查找与当前请求URL匹配的所有权限31   List<SysPermission> sysPermissionList = sysPermissionRepository.findAll();32   List<String> urls = sysPermissionList.stream()33     .map(SysPermission::getUrl)34     .filter(e->pathMatcher.match(e, requestUrl))35     .distinct()36     .collect(Collectors.toList());37 38   if (!CollectionUtils.isEmpty(urls)) {39    return SecurityConfig.createList(urls.toArray(new String[urls.size()]));40   }41 42   return SecurityConfig.createList("ROLE_login");43  }44 45  @Override46  public Collection<ConfigAttribute> getAllConfigAttributes() {47   return null;48  }49 50  @Override51  public boolean supports(Class<?> clazz) {52   return true;53  }54 }

MyAccessDecisionManager.java 

 1 package com.example.demo.service; 2  3 import org.springframework.security.access.AccessDecisionManager; 4 import org.springframework.security.access.AccessDeniedException; 5 import org.springframework.security.access.ConfigAttribute; 6 import org.springframework.security.authentication.AnonymousAuthenticationToken; 7 import org.springframework.security.authentication.InsufficientAuthenticationException; 8 import org.springframework.security.core.Authentication; 9 import org.springframework.security.core.GrantedAuthority;10 import org.springframework.security.web.FilterInvocation;11 import org.springframework.stereotype.Component;12 13 import java.util.Collection;14 import java.util.List;15 import java.util.stream.Collectors;16 17 @Component18 public class MyAccessDecisionManager implements AccessDecisionManager {19 20  /**21   *22   * @param authentication 当前登录用户,可以获取用户的权限列表23   * @param object   FilterInvocation对象,可以获取请求url24   * @param configAttributes25   * @throws AccessDeniedException26   * @throws InsufficientAuthenticationException27   */28  @Override29  public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {30   String requestUrl = ((FilterInvocation) object).getRequestUrl();31   System.out.println(requestUrl);32 33   // 当前用户拥有的权限(能访问的资源)34   Collection<? extends GrantedAuthority> grantedAuthorities = authentication.getAuthorities();35   List<String> authorities = grantedAuthorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());36 37   /*if (!authorities.contains(requestUrl)) {38    throw new AccessDeniedException("权限不足");39   }*/40 41   // 判断访问当前资源所需要的权限用户是否拥有42   // PS: 在我看来,其实就是看两个集合是否有交集43 44   for (ConfigAttribute configAttribute : configAttributes) {45    String attr = configAttribute.getAttribute();46    if ("ROLE_login".equals(attr)) {47     if (authentication instanceof AnonymousAuthenticationToken) {48      throw new AccessDeniedException("非法请求");49     }50    }51 52    if (authorities.contains(attr)) {53     return;54    }55   }56 57   throw new AccessDeniedException("权限不足");58  }59 60  @Override61  public boolean supports(ConfigAttribute attribute) {62   return true;63  }64 65  @Override66  public boolean supports(Class<?> clazz) {67   return true;68  }69 }

最后是WebSecurityConfig

 1 package com.example.demo.config; 2  3 import com.example.demo.handler.MyAccessDeniedHandler; 4 import com.example.demo.handler.MyAuthenticationFailureHandler; 5 import com.example.demo.handler.MyAuthenticationSuccessHandler; 6 import com.example.demo.handler.MyExpiredSessionStrategy; 7 import com.example.demo.service.MyAccessDecisionManager; 8 import com.example.demo.service.MyFilterInvocationSecurityMetadataSource; 9 import com.example.demo.service.MyUserDetailsService;10 import org.springframework.beans.factory.annotation.Autowired;11 import org.springframework.context.annotation.Bean;12 import org.springframework.context.annotation.Configuration;13 import org.springframework.security.config.annotation.ObjectPostProcessor;14 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;15 import org.springframework.security.config.annotation.web.builders.HttpSecurity;16 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;17 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;18 import org.springframework.security.crypto.password.PasswordEncoder;19 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;20 21 @Configuration22 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {23 24  @Autowired25  private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;26  @Autowired27  private MyAuthenticationFailureHandler myAuthenticationFailureHandler;28  @Autowired29  private MyAccessDeniedHandler myAccessDeniedHandler;30  @Autowired31  private MyUserDetailsService myUserDetailsService;32  @Autowired33  private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;34  @Autowired35  private MyAccessDecisionManager myAccessDecisionManager;36 37  @Override38  protected void configure(AuthenticationManagerBuilder auth) throws Exception {39   auth.userDetailsService(myUserDetailsService)40     .passwordEncoder(passwordEncoder());41  }42 43  @Override44  protected void configure(HttpSecurity http) throws Exception {45   http.formLogin()46     .loginProcessingUrl("/login")47     .usernameParameter("username")48     .passwordParameter("password")49     .defaultSuccessUrl("/")50     .successHandler(myAuthenticationSuccessHandler)51     .failureHandler(myAuthenticationFailureHandler)52    .and()53     .authorizeRequests()54     .antMatchers("/login.html", "/login").permitAll()55     .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {56      @Override57      public <O extends FilterSecurityInterceptor> O postProcess(O object) {58       object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);59       object.setAccessDecisionManager(myAccessDecisionManager);60       return object;61      }62     })63    .and()64     .exceptionHandling().accessDeniedHandler(myAccessDeniedHandler)65    .and()66     .sessionManagement().sessionFixation().migrateSession()67     .maximumSessions(1).maxSessionsPreventsLogin(false).expiredSessionStrategy(new MyExpiredSessionStrategy());68  }69 70  @Bean71  public PasswordEncoder passwordEncoder() {72   return new BCryptPasswordEncoder();73  }74 75 }

可以看到,FilterInvocationSecurityMetadataSource的作用就是查找当前请求的资源所对应权限,然后将所需的访问权限列表传给AccessDecisionManager;MyAccessDecisionManager的作用是判断用户是否有权限访问,判断的依据就是当前资源所对应的权限是否在用户所拥有的权限列表中。

在我看来,就是判断两个集合是否有交集,有交集就有权限访问,否则没有权限访问

而且,这种方式的权限在表设计上应该是分了url和权限编码的,也就是说权限标识符是code,不是url。首先,用请求url去匹配权限表,找到与之匹配的权限code,后续所有的权限比较都是比较的权限code。这样其实也挺好。

还有一点,注意到com.example.demo.service.MyAccessDecisionManager#decide()方法有三个参数,第一个参数代表当前登录用户,第二个参数代表用户请求,第三个参数代表访问资源所需的权限。

本例中,用的是第一和第三个参数

但是,我觉得可以直接用第一和第二个参数,用户请求也能拿到,用户权限也能拿到,有这些就可以判断用户是否有权限了,这样的话只需要AccessDecisionManager,而不需要FilterInvocationSecurityMetadataSource了

 

这里补充两点:

1、这里说的权限和资源是一个意思

2、关于资源访问控制,有两种写法。一种是基于权限编码的匹配,另一种是基于url的匹配。

  • 第一种写法是,基于权限编码。即在代码中定义好访问某个资源需要什么样的权限,这里需要用到@PreAuthorize注解。
  • 第二种写法是,基于请求URL。即数据库中配置好资源访问的URL,根据请求URL是否与之匹配来判断。(PS:可以比较权限编码,也可以比较权限URL) 

5.  退出登录

 1 package com.example.demo.config; 2  3 import com.example.demo.handler.*; 4 import com.example.demo.service.MyAccessDecisionManager; 5 import com.example.demo.service.MyFilterInvocationSecurityMetadataSource; 6 import com.example.demo.service.MyUserDetailsService; 7 import org.springframework.beans.factory.annotation.Autowired; 8 import org.springframework.context.annotation.Bean; 9 import org.springframework.context.annotation.Configuration;10 import org.springframework.security.config.annotation.ObjectPostProcessor;11 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;12 import org.springframework.security.config.annotation.web.builders.HttpSecurity;13 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;14 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;15 import org.springframework.security.crypto.password.PasswordEncoder;16 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;17 18 @Configuration19 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {20 21  @Autowired22  private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;23  @Autowired24  private MyAuthenticationFailureHandler myAuthenticationFailureHandler;25  @Autowired26  private MyAccessDeniedHandler myAccessDeniedHandler;27  @Autowired28  private MyLogoutSuccessHandler myLogoutSuccessHandler;29  @Autowired30  private MyUserDetailsService myUserDetailsService;31  @Autowired32  private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;33  @Autowired34  private MyAccessDecisionManager myAccessDecisionManager;35 36  @Override37  protected void configure(AuthenticationManagerBuilder auth) throws Exception {38   auth.userDetailsService(myUserDetailsService)39     .passwordEncoder(passwordEncoder());40  }41 42  @Override43  protected void configure(HttpSecurity http) throws Exception {44   http.formLogin()45     .loginProcessingUrl("/login")46     .usernameParameter("username")47     .passwordParameter("password")48     .defaultSuccessUrl("/")49     .successHandler(myAuthenticationSuccessHandler)50     .failureHandler(myAuthenticationFailureHandler)51    .and().logout()52     .logoutUrl("/logout")53     .logoutSuccessHandler(myLogoutSuccessHandler)54    .and()55     .authorizeRequests()56     .antMatchers("/login.html", "/login").permitAll()57     .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {58      @Override59      public <O extends FilterSecurityInterceptor> O postProcess(O object) {60       object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);61       object.setAccessDecisionManager(myAccessDecisionManager);62       return object;63      }64     })65    .and()66     .exceptionHandling().accessDeniedHandler(myAccessDeniedHandler)67    .and()68     .sessionManagement().sessionFixation().migrateSession()69     .maximumSessions(1).maxSessionsPreventsLogin(false).expiredSessionStrategy(new MyExpiredSessionStrategy());70  }71 72  @Bean73  public PasswordEncoder passwordEncoder() {74   return new BCryptPasswordEncoder();75  }76 77 }

自定义LogoutSuccessHandler

 1 package com.example.demo.handler; 2  3 import com.faster; 4 import org.springframework.security.core.Authentication; 5 import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; 6 import org.springframework.stereotype.Component; 7  8 import javax.servlet.ServletException; 9 import javax.servlet.http.HttpServletRequest;10 import javax.servlet.http.HttpServletResponse;11 import java.io.IOException;12 import java.io.PrintWriter;13 14 @Component15 public class MyLogoutSuccessHandler implements LogoutSuccessHandler {16 17  private static ObjectMapper objectMapper = new ObjectMapper();18 19  @Override20  public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {21 //  response.sendRedirect("/login.html");22 23   response.setContentType("application/json;charset=utf-8");24   PrintWriter printWriter = response.getWriter();25   printWriter.write(objectMapper.writeValueAsString("logout success"));26   printWriter.flush();27   printWriter.close();28  }29 }

到这里为止,我们已经实现了用户动态加载,权限匹配规则动态加载,即谁可以访问什么资源这个过程已经不再是写死了,而是全部可配置化了

6. 集成JWT生成token

现在的项目都是前后端分离的,客户端与服务端通过接口进行交互,数据格式采用JSON,这就要求服务端是无状态的。如果还是利用Session在服务端维持会话的话,可扩展性就太差了。总之一句话,用Session就是有状态的,用Token就是无状态的,因此,我们要用Token来识别用户身份。

默认会话是Session维持的,用Session的话不利于水平扩容(尽管共享Session,但还是很不方便),而且也没法做前后端分离。因此,需要用token来承载认证用户信息,前后端通过json进行交互。

首先,引入依赖

 <dependency>  <groupId>io.jsonwebtoken</groupId>  <artifactId>jjwt</artifactId>  <version>0.9.1</version> </dependency>

然后,JWT工具类

 1 package com.example.demo.util; 2  3 import io.jsonwebtoken.*; 4  5 import java.util.Date; 6 import java.util.HashMap; 7 import java.util.Map; 8 import java.util.function.Function; 9 10 /**11  * @Author ChengJianSheng12  * @Date 2021/5/713  */14 public class JwtUtil {15 16  private static long TOKEN_EXPIRATION = 24 * 60 * 60 * 1000;17  private static String TOKEN_SECRET_KEY = "123456";18 19  /**20   * 生成Token21   * @param subject 用户名22   * @return23   */24  public static String createToken(String subject) {25   long currentTimeMillis = System.currentTimeMillis();26   Date currentDate = new Date(currentTimeMillis);27   Date expirationDate = new Date(currentTimeMillis + TOKEN_EXPIRATION);28 29   // 存放自定义属性,比如用户拥有的权限30   Map<String, Object> claims = new HashMap<>();31 32   return Jwts.builder()33     .setClaims(claims)34     .setSubject(subject)35     .setIssuedAt(currentDate)36     .setExpiration(expirationDate)37     .signWith(SignatureAlgorithm.HS512, TOKEN_SECRET_KEY)38     .compact();39  }40 41  public static String extractUsername(String token) {42   return extractClaim(token, Claims::getSubject);43  }44 45  public static boolean isTokenExpired(String token) {46   return extractExpiration(token).before(new Date());47  }48 49  public static Date extractExpiration(String token) {50   return extractClaim(token, Claims::getExpiration);51  }52 53  public static <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {54   final Claims claims = extractAllClaims(token);55   return claimsResolver.apply(claims);56  }57 58  private static Claims extractAllClaims(String token) {59   return Jwts.parser().setSigningKey(TOKEN_SECRET_KEY).parseClaimsJws(token).getBody();60  }61 62 }

登录成功后,将token返回给客户端

 1 package com.example.demo.handler; 2  3 import com.example.demo.model.MyUserDetails; 4 import com.example.demo.util.JwtUtil; 5 import com.faster; 6 import org.springframework.security.core.Authentication; 7 import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; 8 import org.springframework.stereotype.Component; 9 10 import javax.servlet.ServletException;11 import javax.servlet.http.HttpServletRequest;12 import javax.servlet.http.HttpServletResponse;13 import java.io.IOException;14 15 @Component16 public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {17 18  private static ObjectMapper objectMapper = new ObjectMapper();19 20  @Override21  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {22   MyUserDetails myUserDetails = (MyUserDetails) authentication.getPrincipal();23   String username = myUserDetails.getUsername();24   String token = JwtUtil.createToken(username);25   //todo 缓存到 Redis26   //todo 把token存到Redis中27 28   response.setContentType("application/json;charset=utf-8");29   response.getWriter().write(objectMapper.writeValueAsString(token));30  }31 }

每次请求过来,从token中取到用户信息,然后放到上下文中

 1 package com.example.demo.filter; 2  3 import com.example.demo.service.MyUserDetailsService; 4 import com.example.demo.util.JwtUtil; 5 import org.apache.commons.lang3.StringUtils; 6 import org.springframework.security.authentication.AuthenticationManager; 7 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 8 import org.springframework.security.core.context.SecurityContextHolder; 9 import org.springframework.security.core.userdetails.UserDetails;10 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;11 import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;12 13 import javax.servlet.FilterChain;14 import javax.servlet.ServletException;15 import javax.servlet.http.HttpServletRequest;16 import javax.servlet.http.HttpServletResponse;17 import java.io.IOException;18 19 /**20  * 负责在每次请求中,解析请求头中的token,从中取得用户信息,生成认证对象传递给下一个过滤器21  * @Author ChengJianSheng22  * @Date 2021/5/723  */24 public class JwtAuthenticationFilter extends BasicAuthenticationFilter {25 26  private MyUserDetailsService myUserDetailsService;27 28  public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {29   super(authenticationManager);30  }31 32  public JwtAuthenticationFilter(AuthenticationManager authenticationManager, MyUserDetailsService myUserDetailsService) {33   super(authenticationManager);34   this.myUserDetailsService = myUserDetailsService;35  }36 37  @Override38  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {39   String token = request.getHeader("token");40   System.out.println("请求头中带的token: " + token);41   if (StringUtils.isNoneBlank(token)) {42    if (!JwtUtil.isTokenExpired(token)) {43     String username = JwtUtil.extractUsername(token);44     if (StringUtils.isNoneBlank(username) && null == SecurityContextHolder.getContext().getAuthentication()) {45      // 查询用户权限,有以下三种方式:46      // 1. 可以从数据库中加载47      // 2. 可以从Redis中加载(PS: 前提是之前已经缓存到Redis中了)48      // 3. 可以从token中加载(PS: 前提是生成token的时候把用户权限作为Claims放置其中了)49 50      UserDetails userDetails = myUserDetailsService.loadUserByUsername(username);51 52      UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());53      authRequest.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));54 55      SecurityContextHolder.getContext().setAuthentication(authRequest);56     }57    }58   }59 60   chain.doFilter(request, response);61  }62 }

把这个过滤器添加到

1 http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager(), myUserDetailsService), UsernamePasswordAuthenticationFilter.class);

完整配置如下:

 1 package com.example.demo.config; 2  3 import com.example.demo.filter.JwtAuthenticationFilter; 4 import com.example.demo.handler.*; 5 import com.example.demo.service.MyAccessDecisionManager; 6 import com.example.demo.service.MyFilterInvocationSecurityMetadataSource; 7 import com.example.demo.service.MyUserDetailsService; 8 import org.springframework.beans.factory.annotation.Autowired; 9 import org.springframework.context.annotation.Bean;10 import org.springframework.context.annotation.Configuration;11 import org.springframework.security.config.annotation.ObjectPostProcessor;12 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;13 import org.springframework.security.config.annotation.web.builders.HttpSecurity;14 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;15 import org.springframework.security.config.http.SessionCreationPolicy;16 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;17 import org.springframework.security.crypto.password.PasswordEncoder;18 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;19 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;20 21 @Configuration22 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {23 24  @Autowired25  private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;26  @Autowired27  private MyAuthenticationFailureHandler myAuthenticationFailureHandler;28  @Autowired29  private MyAccessDeniedHandler myAccessDeniedHandler;30  @Autowired31  private MyLogoutSuccessHandler myLogoutSuccessHandler;32  @Autowired33  private MyUserDetailsService myUserDetailsService;34  @Autowired35  private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;36  @Autowired37  private MyAccessDecisionManager myAccessDecisionManager;38  @Autowired39  private MyAuthenticationEntryPoint myAuthenticationEntryPoint;40 41  @Override42  protected void configure(AuthenticationManagerBuilder auth) throws Exception {43   auth.userDetailsService(myUserDetailsService)44     .passwordEncoder(passwordEncoder());45  }46 47  @Override48  protected void configure(HttpSecurity http) throws Exception {49   http.formLogin()50     .loginProcessingUrl("/login")51     .usernameParameter("username")52     .passwordParameter("password")53     .successHandler(myAuthenticationSuccessHandler)54     .failureHandler(myAuthenticationFailureHandler)55    .and().logout()56     .logoutUrl("/logout")57     .logoutSuccessUrl("/login.html")58     .logoutSuccessHandler(myLogoutSuccessHandler)59    .and()60     .authorizeRequests()61     .antMatchers("/login.html", "/login").permitAll()62     .anyRequest().access("@myAccessDecisionService.hasPermission(request, authentication)")63    .and()64     .exceptionHandling().accessDeniedHandler(myAccessDeniedHandler).authenticationEntryPoint(myAuthenticationEntryPoint)65    .and()66     .sessionManagement().sessionFixation().migrateSession().sessionCreationPolicy(SessionCreationPolicy.STATELESS)67     .maximumSessions(1).maxSessionsPreventsLogin(false).expiredSessionStrategy(new MyExpiredSessionStrategy());68 69   http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager(), myUserDetailsService), UsernamePasswordAuthenticationFilter.class);70 71   http.csrf().disable();72  }73 74  @Bean75  public PasswordEncoder passwordEncoder() {76   return new BCryptPasswordEncoder();77  }78 79 }

增加一个未登录的处理

 1 package com.example.demo.handler; 2  3 import com.faster; 4 import org.springframework.security.core.AuthenticationException; 5 import org.springframework.security.web.AuthenticationEntryPoint; 6 import org.springframework.stereotype.Component; 7  8 import javax.servlet.ServletException; 9 import javax.servlet.http.HttpServletRequest;10 import javax.servlet.http.HttpServletResponse;11 import java.io.IOException;12 13 /**14  * 未认证(未登录)统一处理15  * @Author ChengJianSheng16  * @Date 2021/5/717  */18 @Component19 public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {20 21  private static ObjectMapper objectMapper = new ObjectMapper();22 23  @Override24  public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {25   response.setContentType("application/json;charset=utf-8");26   response.getWriter().write(objectMapper.writeValueAsString("未登录,请先登录"));27  }28 }

改造后的项目结构如下

最后,用token以后,退出要做一点改动。由于我们采用JWT来生成Token,因此token是没法撤销和删除的,所以此时的退出应该是:

  1. Token生成以后要保存到数据库(MySQL或者Redis)
  2. 每次请求要校验Token是否存在及有效
  3. 退出登录后删除数据库中保存的Token

关于Spring Security实现简单的用户、角色、权限控制就先讲到这里,稍微做一个回顾:

  1. 未认证(登录)的用户提示他要先登录
  2. 已认证的用户判断是否有权限访问

 









原文转载:http://www.shaoqun.com/a/732778.html

跨境电商:https://www.ikjzd.com/

塔图:https://www.ikjzd.com/w/2274

pat:https://www.ikjzd.com/w/1079


本文是一个笔记系列,目标是完成一个基于角色的权限访问控制系统(RBAC),有基本的用户、角色、权限管理,重点在SpringSecurity的各种配置。万丈高楼平地起,接下来,一步一步,由浅入深,希望给一起学习的小伙伴一个参考。1.HelloSecurity按照惯例,先写个HelloWorld首先,引入依赖1<dependency>2<groupId>org.springfr
周宁:https://www.ikjzd.com/w/1647
吉祥邮:https://www.ikjzd.com/w/1565
走秀网:https://www.ikjzd.com/w/2427
"真黑五"来袭,亚马逊海外购以商业创新迎跨境电商"风口":https://www.ikjzd.com/home/135097
女友让我爸妈还清我们的房贷:http://www.30bags.com/m/a/253339.html
【运营实操】如何快速的增加instagram粉丝:https://www.ikjzd.com/tl/103772

No comments:

Post a Comment