这篇文章是在看了《Spring Boot+Vue全栈开发实战》后做的一些总结和笔记,要继续加油啊,奥利给!!!
文章目录
- 1.创建一个SpringBootWeb项目,添加spring-boot-starter-security和Mybatis依赖。
- 2.自定义类继承自WebSecurityConfigurerAdapter,实现对SpringSecurity的更多配置。
- 3.基于数据库的认证——定义`Role类`和`实现UserDetails接口的User类`
- 4. 基于数据库的认证——定义实现UserDetailsService接口的Service层
- 5.基于数据库的认证——UserMapper,RoleMapper
- 6.动态权限配置——定义Menu类
- 7.动态权限配置——自定义FilterInvocationSecurityMetadataSource
- 8.动态权限配置——自定义AccessDecisionManager
- 9.动态权限配置——MenuMapper
1.创建一个SpringBootWeb项目,添加spring-boot-starter-security和Mybatis依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
2.自定义类继承自WebSecurityConfigurerAdapter,实现对SpringSecurity的更多配置。
@Configuration
/**
* @EnableGlobalMethodSecurity注解开启基于注解的安全配置,即面向方法的认证与授权,而非基于URL。
* prePostEnabled=true会解锁@PreAuthorize和@PostAuthorize两个注解,分别在方法执行前和执行后进行验证。
* securedEnabled=true会解锁@Secured注解。
*
* @Service
* public class MethodService {
* @Secured("ROLE_ADMIN")
* 访问该方法需要ADMIN角色,此方法需要在角色前加ROLE_。
* public String admin() {
* return "admin";
* }
* @PreAuthorize("hasRole('ADMIN') and hasRole('DBA')")
* public String dba() {
* return "dba";
* }
* @PreAuthorize("hasAnyRole('ADMIN','DBA','ADMIN')")
* public String user() {
* return "user";
* }
* }
*/
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
private CustomMetadataSource metadataSource;
@Autowired
private CustomAccessDecisionManager accessDecisionManager;
/**
* 不在AuthenticationManagerBuilder中配置passwordEncoder,可以直接使用@Bean配置。
* 此处使用的加密方式:不加密。
*/
/*@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}*/
/**
* 角色继承
* 若要ROLE_admin具有admin和user的权限,可用角色继承配置。
*/
/*@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_dba > ROLE_admin ROLE_admin > ROLE_user";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}*/
/**
* AuthenticationManagerBuilder用来配置全局的认证相关的信息,
* 其实就是AuthenticationProvider和UserDetailsService,前者是认证服务提供者,后者是认证用户(及其权限)。
*
* 这里配置了UserDetailsService(从数据库获取账户密码用于对比)和PasswordEncoder(密码加密方式)
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/**
* userService是一个实现了UserDetailsService接口的实体类。
*
* BCryptPasswordEncoder是官方推荐的,使用BCrypt强哈希函数。
* 使用时可选择提供strength和SecureRandom实例。
* strength越大,密钥迭代次数越多,迭代次数为^strength,strength取值在4~31之间默认为10。
* 一般用户密码存在数据库中,所以要在注册时对密码进行加密处理:
* BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
* String encode = encoder.encode(password);
*/
auth.userDetailsService(hrService).passwordEncoder(new BCryptPasswordEncoder());
/*不使用UserDetailService,手动配置账户密码角色*/
/*auth.inMemoryAuthentication()
.withUser("admin").password("123").roles("ADMIN","USER")
.and()
.withUser("123").password("123").roles("USER");*/
}
/**
* HttpSecurity 具体的权限控制规则配置,例如什么URL需要什么角色。
* 可在此配置受保护资源,以及根据实际情况进行角色管理。
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/* 把权限控制规则放在数据库中,数据库中的角色需要ROLE_前缀!!! */
http.authorizeRequests()
/*在定义FilterSecurityInterceptor时,将动态权限配置的2个实例设置进去*/
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(metadataSource);
object.setAccessDecisionManager(accessDecisionManager);
return object;
}
})
.and()
.formLogin().loginPage("/login_p").loginProcessingUrl("/login")
/*定义登录成功的处理逻辑*/
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest
, HttpServletResponse httpServletResponse
, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
/*通过Authentication可以获取当前登录用户的信息*/
Object principal = authentication.getPrincipal();
/*Authentication也可以通过SecurityContextHolder.getContext().getAuthentication()获得*/
Object principal1 = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
ObjectMapper objectMapper = new ObjectMapper();
String value = objectMapper.writeValueAsString(principal);
PrintWriter writer = httpServletResponse.getWriter();
writer.write(value);
writer.flush();
writer.close();
}
})
/*定义登录失败的逻辑*/
/*可通过AuthenticationException获取登录失败的原因,进而给用户明确提示。*/
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest
, HttpServletResponse httpServletResponse
, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
String respBean = "";
if (e instanceof BadCredentialsException || e instanceof UsernameNotFoundException) {
respBean = "用户名或密码错误!";
} else if (e instanceof LockedException) {
respBean = "账户被锁定,请联系管理员!";
} else if (e instanceof CredentialsExpiredException) {
respBean = "密码过期,请联系管理员!";
} else if (e instanceof AccountExpiredException) {
respBean = "账户过期,请联系管理员!";
} else if (e instanceof DisabledException) {
respBean = "账户被禁用,请联系管理员!";
} else {
respBean = "登录失败!";
}
/*401:用户没有访问权限*/
httpServletResponse.setStatus(401);
ObjectMapper objectMapper = new ObjectMapper();
String value = objectMapper.writeValueAsString(respBean);
PrintWriter writer = httpServletResponse.getWriter();
writer.write(value);
writer.flush();
writer.close();
}
})
.permitAll()
.and()
.logout().permitAll()
.and()
/*关闭csrf*/
.csrf().disable()
});
/*直接在方法中手动确定权限规则控制*/
/*authorizeRequests()开启HttpSecurity的配置*/
// http.authorizeRequests()
// .antMatchers("/admin/**")
// .hasRole("ADMIN")
// .antMatchers("/user/**")
// .access("hasAnyRole('ADMIN','USER')")
// .antMatchers("/db/**")
// .access("hasRole('ADMIN') and hasRole('DBA')")
// //除了前面配置过的URL路径外,访问其他URL必须登录后访问
// .anyRequest().authenticated()
// .and()
// //formLogin()开启表单登录。
// .formLogin()
// //loginPage("/login_page"):若用户访问一个需授权才能访问的接口,会跳转至login_page这个自定义页面进行登录,而非默认登录页面。
// .loginPage("/login_page")
// //loginProcessingUrl("/login")配置了登录接口为"/login"(即调用/login接口,发起POST请求进行登录)。
// .loginProcessingUrl("/login")
// //接口的登录参数中用户名默认是username,密码默认是password,可通过usernameParameter和passwordParameter手动配置。
// .usernameParameter("username").passwordParameter("password")
// //permitAll()表示和登录相关的接口不需要认证
// .permitAll()
// .and()
// //开启注销登录的配置
// .logout()
// //配置注销登录请求URL,默认是"/logout"
// .logoutUrl("/logout")
// //配置注销是否清除身份认证信息,默认为ture,表示清除
// .clearAuthentication(true)
// //配置注销是否使Session失效,默认为ture
// .invalidateHttpSession(true)
// //配置一个LogoutHandler,可以在其中完成一些数据清除工作,例如Cookie的清除
// //SpringSecurity提供了一些常见的LogoutHandler的实现类
// .addLogoutHandler(new LogoutHandler() {
// @Override
// public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
// //数据清除
// }
// })
// //定义注销成功后的逻辑
// .logoutSuccessHandler(new LogoutSuccessHandler() {
// @Override
// public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
// //注销后的业务逻辑
// }
// });
}
}
3.基于数据库的认证——定义Role类
和实现UserDetails接口的User类
public class Role{
private Integer id;
private String name;
private String nameZh;
//getter and setter
}
public class User implements UserDetails {
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<GrantedAuthority> list = new ArrayList<>();
for (Role role : roles) {
list.add(new SimpleGrantedAuthority(role.getName()));
}
return list;
}
@JsonIgnore
@Override
/*获取当前对象密码*/
/*前端用户在登录成功后,需要获取当前用户的信息,对于一些敏感信息不必返回,使用@JsonIgnore注解即可*/
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;
}
//getter and setter
}
4. 基于数据库的认证——定义实现UserDetailsService接口的Service层
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
/**
* 本类实现UserDetailsService接口,实现了loadUserByUsername方法,参数是用户登录输入的用户名。
* 通过用户名去数据库查找用户,若没找到用户,抛出用户不存在异常;
* 若找到用户,继续查找用户所具有的角色信息,将获取到的用户对象返回,再由系统提供的DaoAuthenticationProvider类去对比密码是否正确。
* loadUserByUsername方法在用户登录时自动调用。
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserWithRolesByUsername(username);
System.out.println("loadUserByUsername: " + hr);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
return user;
}
}
5.基于数据库的认证——UserMapper,RoleMapper
(1)实现UserDetailsService接口的Service层所用到的UserMapper,包含根据用户名查找用户(含角色信息)
的方法。
public interface UserMapper {
/**
* 根据username查询User实体
*/
@Select("select * from user where username=#{username}")
@Results({
@Result(property = "id", column = "id", id = true),
@Result(property = "roles", column = "id", many = @Many(select = "com.zio.dao.RoleMapper.findRolesByUserId"))
})
User loadUserWithRolesByUsername(@Param("username") String username);
}
(2)UserMapper中loadUserWithRolesByUsername方法用到的RoleMapper
public interface RoleMapper {
@Select("select r.* from user_role ur,role r where ur.rid=r.id and ur.userid=#{userId}")
List<Role> findRolesByUserId(@Param("userId") Integer userId);
/*此方法在后面被MenuMapper调用*/
@Select("select r.* from menu_role mr,role r where mr.rid=r.id and mr.mid=#{menuId}")
List<Role> findRolesByMenuId(@Param("menuId") Integer menuId);
}
6.动态权限配置——定义Menu类
public class Menu{
private Integer id;
private String pattern;
private List<Role> roles;
//gettter and setter
}
7.动态权限配置——自定义FilterInvocationSecurityMetadataSource
/**
* FilterInvocationSecurityMetadataSource的默认实现类是DefaultFilterInvocationSecurityMetadataSource。
* 一个请求走完FilterInvocationSecurityMetadataSource中的getAttributes方法后,会来到AccessDecisionManager类中进行角色信息的对比。
*/
@Component
public class CustomMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private MenuMapper menuMapper;
/**
* getAttributes方法确定一个请求需要哪些角色。
*
* 用户访问时,通过此方法获取所访问的URL所需的角色。
* 此方法若返回null,代表这个请求不需要任何角色就能访问。
* 注意:登录处理过程的URL不走此方法。
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String url = ((FilterInvocation) object).getRequestUrl();
if ("/login_p".equals(url)) {
return null;
}
/*创建一个AntPathMatcher,用于实现ant风格的URL匹配*/
AntPathMatcher matcher = new AntPathMatcher();
List<Menu> allMenu = menuMapper.findAllMenus();
for (Menu menu : allMenu) {
if (matcher.match(menu.getUrl(), url) && !CollectionUtils.isEmpty(menu.getRoles())) { //URL匹配成功且需要角色
System.out.println("匹配成功: menuUrl:" + menu.getUrl() + " url:" + url);
List<Role> roles = menu.getRoles();
int size = roles.size();
String[] array = new String[size];
for (int i = 0; i < size; i++) {
array[i] = roles.get(i).getName();
}
return SecurityConfig.createList(array);
}
}
//若未匹配成功的URL,均为登录后访问
return SecurityConfig.createList("ROLE_LOGIN");
}
/**
* 返回定义好的权限资源,SpringSecurity在启动时校验相关配置是否正确,若不需校验,返回null即可。
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
/**
* 判断类对象是否支持校验。
*/
@Override
public boolean supports(Class<?> clazz) {
//源类.isAssignableFrom(目标类) 用于判断目标类是否是源类的子类
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
8.动态权限配置——自定义AccessDecisionManager
/**
* 一个请求走完FilterInvocationSecurityMetadataSource中的getAttributes方法后,会来到AccessDecisionManager类中进行角色信息的对比。
*/
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
/**
* 判断当前登录的用户是否具备当前请求URL所需要的角色信息
* 不具备,抛出AccessDeniedException异常
* 具备,不做任何事
*
* @param authentication 包含当前登录用户的信息
* @param object 是一个FilterInvocation对象,可以获取当前请求URL等
* @param configAttributes FilterInvocationSecurityMetadataSource中getAttributes方法的返回值,即当前请求URL所需要的角色
* @throws AccessDeniedException
* @throws InsufficientAuthenticationException
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
//循环判断访问该URL所需的每个角色,用户拥有其中一个即可访问
for (ConfigAttribute configAttribute : configAttributes) {
String needRole = configAttribute.getAttribute();
if ("ROLE_LOGIN".equals(needRole)) { //只需登录就能访问
if (authentication instanceof AnonymousAuthenticationToken) { //判断authentication是否为匿名用户实例
throw new AccessDeniedException("未登录");
} else { //已登录即放行
return;
}
}
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("权限不足");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
9.动态权限配置——MenuMapper
public interface MenuMapper {
@Select("select * from menu")
@Results({
@Result(property = "id", column = "id", id = true),
@Result(property = "roles", column = "id", many = @Many(select = "com.zio.dao.RoleMapper.findRolesByMenuId"))
})
List<Menu> findAllMenus();
}