SpringSecurity的基本上手学习
1. SpringSecurity的基本配置
-
导入SpringSecurity包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
-
只要导入了SpringSecurity,项目所有资源都会被保护起来了
添加一个hello接口
@GetMapping("/hello") public String hello() { return "Hello"; }
-
启动项目后,访问/hello接口,会自动跳转到登录页面,这个登录页面是由SpringSecurity提供的。
默认的用户名是
user
,默认的登录密码则在每次启动项目时随机生成,在终端打印。 -
配置用户名和密码
如果对于默认的用户名和密码不满意(肯定不满意,每次都是随机生成的密码),可以在配置文件中配置默认的用户名和密码,用户角色
spring: security: user: name: admin password: 123 roles: - admin
2. 基于内存的认证
-
通过自定义类继承
WebSecurityConfigurerAdapter
,实现configure(AuthenticationManagerBuilder auth)
方法自定义,如下基于内存的认证:@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception{ auth.inMemoryAuthentication() .passwordEncoder(new BCryptPasswordEncoder()) .withUser("root") .password(new BCryptPasswordEncoder().encode("123")) .roles("ADMIN", "DBA") .and() .withUser("admin") .password(new BCryptPasswordEncoder().encode("123")) .roles("ADMIN", "USER") .and() .withUser("sang") .password(new BCryptPasswordEncoder().encode("123")) .roles("USER"); }
注意:在基于内存的用户配置中,配置角色时不需要添加"ROLE_"前缀!
接下来的基于数据库的认证,角色需要带上ROLE_,比如ROLE_admin,ROLE_user
-
WebSecurityConfigurerAdapter
类有三个configure方法,方法参数不同,上面设置认证相关的是AuthenticationManagerBuilder
,Http请求相关的需要重写参数为HttpSecurity
的configure方法@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/user/**").access("hasAnyRole('ADMIN', 'USER')") .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") .anyRequest() .authenticated() .and() .formLogin() .loginPage("/login_page") // 登录页面,默认为login_page .loginProcessingUrl("/login") // 登录请求接口,没有设置的话默认与loginPage一致 .usernameParameter("name") // 请求参数用户名的参数名字 .passwordParameter("passwd") // 请求参数密码的参数名字 .successHandler(new AuthenticationSuccessHandler() { // 登录成功的一些处理 @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // 登录用户对象信息 Object principal = authentication.getPrincipal(); response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); response.setStatus(200); Map<String, Object> map = new HashMap<>(); map.put("status", 200); map.put("msg", principal); // json转换 ObjectMapper om = new ObjectMapper(); out.write(om.writeValueAsString(map)); out.flush(); out.close(); } }) .failureHandler(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(); Map<String, Object> map = new HashMap<>(); map.put("status", 401); if (exception instanceof LockedException) { map.put("msg", "账户被锁定,登陆失败!"); } else if (exception instanceof BadCredentialsException) { map.put("msg", "账户名或密码输入错误,登录失败!"); } else if (exception instanceof DisabledException) { map.put("msg", "账户被禁用,登录失败!"); } else if (exception instanceof AccountExpiredException) { map.put("msg", "账户已过期,登录失败!"); } else if (exception instanceof CredentialsExpiredException) { map.put("msg", "密码已过期,登录失败!"); } else { map.put("msg", "登录失败!"); } ObjectMapper objectMapper = new ObjectMapper(); out.write(objectMapper.writeValueAsString(map)); out.flush(); out.close(); } }) .permitAll() // 表示和登录相关的接口不需要认证可直接访问 // 注销登录 .and() .logout() .logoutUrl("/logout") .clearAuthentication(true) // 清除身份认证信息 .invalidateHttpSession(true) // 使session失效 .addLogoutHandler(new LogoutHandler() { @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { // 进行一些注销登录的处理 } }) .logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.sendRedirect("/login_page"); } }) .and() .csrf() .disable(); }
代码解释:
- 调用
authorizeRequests()
方法开启HttpSecurity的配置 antMatchers()
和hasRole()
使用ANT匹配URL,表示访问“xxx"需要具备xxx角色
- 调用
3. 基于数据库的认证
-
设计数据表,新建一个user,role以及user_role表
注意:角色名有一个默认的前缀:ROLE_
-
配置mybatis,数据库连接等
-
分别创建用户表和角色表对应的实体类
@Data public class Role { private Integer id; private String name; private String nameZh; }
@Data public class User implements UserDetails { private static final long serialVersionUID = 7740365774291023439L; private Integer id; private String username; private String password; private Boolean enabled; private Boolean locked; private List<Role> roles; @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<SimpleGrantedAuthority> authorities = new ArrayList<>(); for (Role role : roles) { authorities.add(new SimpleGrantedAuthority(role.getName())); } System.out.println("authorities = " + authorities); return authorities; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return !locked; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enabled; } }
-
用户实体类需要实现UserDetails接口,并实现接口中的7个方法
-
我们根据实际情况设置这7个方法的返回值,因为默认情况下不需要开发者自己进行密码角色等信息的比对,开发者只需要提供相关信息即可
-
getAuthorities()
方法用来获取当前用户所具有的的角色信息,我们这里角色存储在roles属性中,所以这个方法里直接遍历roles属性,然后构造SimpleGrantedAuthority
集合并返回
-
-
创建UserService,实现
UserDetailsService
接口@Service public class UserService implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.loadUserByUsername(username); if (user == null) { throw new UsernameNotFoundException("账号不存在!"); } user.setRoles(userMapper.getUserRolesByUid(user.getId())); System.out.println("user = " + user); return user; } }
-
实现
UserDetailsService
接口,并实现了接口中的loadUserByUsername
方法,该方法的参数就是用户登录时输入的用户名,通过用户名去查找数据库,如果查不到这个用户则抛出异常,如果查到了,则继续查找该用户具有的角色信息,并将获取到的user对象返回,由系统提供的DaoAuthenticationProvider
类去比对密码是否正确也就是通过用户名查找用户,找得到就查下角色信息,将这个用户对象返回,系统会拿用户输入的密码与查询返回的用户对象密码去比对。
-
loadUserByUsername
方法将在用户登录时自动调用
-
-
最后对SpringSecurity进行配置
不再使用基于内存的配置方式了,将刚刚创建的UserService配置到
AuthenticationManagerBuilder
中@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); }
4. 角色继承
在之前的例子中定义了三种角色,但是三种角色之间是没有任何关系的,一般来说角色权限是存在一定关系的,比如ROLE_admin一般是既具有admin权限,也具有user的权限的。
如何配置这种角色继承关系?
在SpringSecurity中只需要提供一个RoleHierarchy
即可
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
/**
* 角色继承
* @return
*/
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
// springboot2.0.8(包含2.0.8)之前的格式
// String hierarchy = "ROLE_dba > ROLE_admin ROLE_admin > ROLE_user";
// springboot2.0.8之后版本的格式
String hierarchy = "ROLE_dba > ROLE_admin \n ROLE_admin > ROLE_user";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
}
注意:springboot2.0.8之后的版本中,写法是使用\n
分隔符,之前的版本是使用空格
5. 动态配置权限
-
数据库设计
新增一个menu表,存放资源路径以及一个资源角色表menu_role
-
自定义
FilterInvocationSecurityMetadataSource
Spring Security通过
FilterInvocationSecurityMetadataSource
接口中的getAttributes
方法来确定一个请求需要哪些角色。FilterInvocationSecurityMetadataSource
接口默认实现类是:DefaultFilterInvocationSecurityMetadataSource
.参考该类的实现,可以定义自己的FilterInvocationSecurityMetadataSource.public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private AntPathMatcher antPathMatcher = new AntPathMatcher(); @Autowired private MenuMapper menuMapper; @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { String requestUrl = ((FilterInvocation) object).getRequestUrl(); List<Menu> allMenus = menuMapper.getAllMenus(); for (Menu menu : allMenus) { if (antPathMatcher.match(menu.getPattern(), requestUrl)) { List<Role> roles = menu.getRoles(); String[] roleArr = new String[roles.size()]; for (int i = 0; i < roleArr.length; i++) { roleArr[i] = roles.get(i).getName(); } // 返回角色集合 return SecurityConfig.createList(roleArr); } } return SecurityConfig.createList("ROLE_LOGIN"); } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } }
-
自定义
AccessDecisionManager
当一个请求走完
FilterInvocationSecurityMetadataSource
的getAttributes方法后,接下来就会来到AccessDecisionManager
类中进行角色信息的对比,可自定义AccessDecisionManager
public class CustomAccessDecisionManager implements AccessDecisionManager { /** * 判断当前登录的用户是否具备当前请求url所需要的角色信息 * @param authentication 当前登录用户的信息 * @param object 是一个FilterInvocation对象,可以获取当前请求对象 * @param configAttributes FIlterInvocationSecurityMetadataSource中的getAttributes方法的返回值,也就是当前请求url所需要的角色 * @throws AccessDeniedException * @throws InsufficientAuthenticationException */ @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { Collection<? extends GrantedAuthority> auths = authentication.getAuthorities(); for (ConfigAttribute configAttribute : configAttributes) { if ("ROLE_LOGIN".equals(configAttribute.getAttribute()) && authentication instanceof UsernamePasswordAuthenticationToken) { return; } for (GrantedAuthority authority : auths) { if (configAttribute.getAttribute().equals(authority.getAuthority())) { return; } } } throw new AccessDeniedException("权限不足"); } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return true; } }
-
配置
完成两个自定义实现类后,需要在SpringSecurity中配置
/** * 配置动态权限 */ @Bean public CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource() { return new CustomFilterInvocationSecurityMetadataSource(); } @Bean public CustomAccessDecisionManager customAccessDecisionManager() { return new CustomAccessDecisionManager(); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // .antMatchers("/admin/**").hasRole("admin") // .antMatchers("/db/**").hasRole("dba") // .antMatchers("/user/**").hasRole("user") // .anyRequest().authenticated() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource()); object.setAccessDecisionManager(customAccessDecisionManager()); return object; } }) .and() .formLogin() .loginProcessingUrl("/login").permitAll() .and() .csrf().disable(); }
注意:实现自定义动态权限,之前的角色继承就失效了,所需角色权限在数据库中配置即可