no15-springsecurity学习
分类: springboot 专栏: springboot学习 标签: springsecurity
2023-04-09 22:36:14 760浏览
springsecurity登录原理:
1. 登录的时候,会经过一个过滤器叫做 SecurityContextPersistenceFilter,当用户登录成功后,会将用户信息存入 SecurityContextHolder 中(SecurityContextHolder 默认底层是将用户数据存入到 ThreadLocal 中的),然后在登录请求结束的时候,在 SecurityContextPersistenceFilter 过滤器中,会将 SecurityContextHolder 中的用户信息读取出来存入到 HttpSession 中。
2. 以后每次用户发起请求的时候,都会经过 SecurityContextPersistenceFilter,在这个过滤器中,系统会从 HttpSession 中读取出来当前登录的用户信息并存入 SecurityContextHolder 中。接下来进行后续的业务处理,在后续的处理中,凡是需要获取当前用户信息的,都从 SecurityContextHolder 中直接获取。当当前请求结束的时候,就会将 SecurityContextHolder 中的信息清除,下一次请求来的时候,重复步骤2。
Spring Security过滤器链结构图
FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器
(AccessDecisionManager)进行处理
FilterChainProxy相关类的UML图示
spring Security功能的实现主要是由一系列过滤器链相互配合完成
下面介绍过滤器链中主要的几个过滤器及其作用:
SecurityContextPersistenceFilter 这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截
器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给
SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好
的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
UsernamePasswordAuthenticationFilter(处理表单登录) 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密
码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和
AuthenticationFailureHandler,这些都可以根据需求做相关改变;
FilterSecurityInterceptor 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了;
ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:
AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
LogoutFilter:Spring Security 提供了一个默认的登出过滤器 LogoutFilter,默认拦截路径是 /logout,当访问 /logout 路径的时候,LogoutFilter 会进行退出处理。
DefaultLoginPageGeneratingFilter:默认登录页面
DefaultLogoutPageGeneratingFilter:默认注销页面
认证流程
授权流程
springboot集成springsecurity
引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency>
只要引入了security的依赖,默认所有的页面都被拦截到他的登录页面(访问路径是/login,退出登录是/logout)。默认账号是user,密码是启动的时候下图打印的。
配置类-认证部分
这里提一下:高版本的springboot项目WebSecurityConfigurerAdapter过时了,参考文章:https://www.45fan.com/article.php?aid=1D2FQCfQWb30v4IH
采用明文的方式认证
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 如果采用明文的话,这个不用配置也是可以的改下面的密码前加{noop} * .password("{noop}123456") * @return */ @Bean PasswordEncoder passwordEncoder(){ return NoOpPasswordEncoder.getInstance();//采用明文的方式 } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("root") .password("123456") .roles("vip1", "vip2", "vip3") .and() .withUser("admin") .password("123456") .roles("vip1", "vip2") .and() .withUser("wang") .password("123456") .roles("vip1"); } }
采用加盐密文的方式
//生成123456的加盐密文 BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); System.out.println(passwordEncoder.encode("123456")); //验证明文和密文是否匹配 passwordEncoder.matches("123456","$2a$10$hkm7QXGV6dUBlN/a9FUVmO5fV.GBeN3WQursxAf3nXIGCE.6YN3he")); }
/** * 如果采用明文的话,这个不用配置也是可以的改下面的密码前加{noop} * .password("{noop}123456") * @return */ @Bean BCryptPasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder();//采用明文的方式 } /** * 认证 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("root") .password("$2a$10$hkm7QXGV6dUBlN/a9FUVmO5fV.GBeN3WQursxAf3nXIGCE.6YN3he") .roles("vip1", "vip2", "vip3") .and() .withUser("admin") .password("$2a$10$hkm7QXGV6dUBlN/a9FUVmO5fV.GBeN3WQursxAf3nXIGCE.6YN3he") .roles("vip1", "vip2") .and() .withUser("wang") .password("$2a$10$hkm7QXGV6dUBlN/a9FUVmO5fV.GBeN3WQursxAf3nXIGCE.6YN3he") .roles("vip1"); }
配置类-授权部分
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 授权 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() //设置白名单 .antMatchers("/").permitAll() .antMatchers("/toLogin").permitAll() .antMatchers("/level1/**").hasRole("vip1") .antMatchers("/level2/**").hasRole("vip2") .antMatchers("/level3/**").hasRole("vip3") //处了以上两个资源,其他资源都要经过认证(登录) .anyRequest().authenticated(); http.formLogin() .loginPage("/toLogin")////设置登录页面 .loginProcessingUrl("/userlogin")//登录表单提交的action设置对应.默认是login .usernameParameter("uname")//默认不配置的话是username .passwordParameter("pwd")//默认不配置的话是password .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.sendRedirect(request.getContextPath()+"/");//登录成功后直接跳转到首页(如果不设置的话是跳到上一个访问页) } }) //或者 .defaultSuccessUrl("/",true)//登录成功后跳的页面 .permitAll(); //开启自动配置的注销功能。 http.logout().loginUrl("/logout").logoutSuccessUrl("/");//注销成功以后来到首页 //1、访问 /logout 表示用户注销,清空session //2、注销成功会返回 /login?logout 页面; //开启记住我功能 http.rememberMe().rememberMeParameter("remeber"); //登陆成功以后,将cookie发给浏览器保存,以后访问页面带上这个cookie,只要通过检查就可以免登录 //点击注销会删除cookie } }
页面动态化
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5" 命名空间
sec:authorize="isAuthenticated()" 判断是否登录
sec:authentication="name" 显示用户名
sec:authentication="principal.authorities" 显示当前用户角色名
sec:authorize="hasRole('vip1')" 判断是否具有什么角色
sec:authorize="hasAnyRole('vip1','vip2','vip3')" 判断是否具有这些角色中的一个
<h2 align="center" sec:authorize="!isAuthenticated()" >游客您好,如果想查看武林秘籍 <a th:href="@{/toLogin}">请登录</a></h2> <div sec:authorize="isAuthenticated()"> <h2> <span sec:authentication="name"></span>,您好,您的角色有: <span sec:authentication="principal.authorities"></span> </h2> <form th:action="@{/logout}" method="post"> <input type="submit" value="注销"/> </form> </div>
关联数据库
- 1.授权部分
service实现类要实现UserDetailsService接口。重写loadUserByUsername方法
@Service public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService, UserDetailsService { @Autowired private UserInfoMapper userInfoMapper; @Autowired private UserRoleMapper userRoleMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Map<String,Object> map = new HashMap<>(); map.put("uname",username); List<UserInfo> users = userInfoMapper.selectByMap(map); List<UserRole> roles = userRoleMapper.getUserRoleByUname(username); if (users==null || users.size() ==0 ){ throw new UsernameNotFoundException("用户不存在"); } List<SimpleGrantedAuthority> authorities = new ArrayList<>(); //遍历当前用户的角色集合组装权限 for (UserRole role : roles) { authorities.add(new SimpleGrantedAuthority(role.getRoleName())); } return new User(username,users.get(0).getPwd(),authorities);//如果用户没有角色会NullPointerException } }
换一种写法:用户实体类要实现
implements UserDetails @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<SimpleGrantedAuthority> list = new ArrayList<>(); roles.forEach(r -> { SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(r.getRoleName()); list.add(simpleGrantedAuthority); }); return list; }
@Service public class UserinfoServiceImpl extends ServiceImpl<UserinfoMapper, Userinfo> implements UserinfoService, UserDetailsService { @Resource UserinfoMapper userinfoMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Userinfo user = new Userinfo(); user.setUname(username); Userinfo userinfo = userinfoMapper.getBy(user); if (ObjectUtils.isEmpty(userinfo) ){ throw new UsernameNotFoundException("用户不存在"); } return userinfo; } }
security配置类认证部分修改
- 2.改造授权
//spring security登录成功后跳转回登录前的页面(解决方案) http.formLogin() .loginPage("/toLogin") .loginProcessingUrl("/login") .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.sendRedirect(request.getContextPath()+"/"); } }) .permitAll(); http.authorizeRequests() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { //查找访问当前url需要什么角色(权限) object.setSecurityMetadataSource(securityMetadataSourcel); //判断(裁决)当前用户有没有这个角色(权限) object.setAccessDecisionManager(accessDecisionManager); return object; } });
@Component public class GongFuSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { @Autowired MenuMapper menuMapper; AntPathMatcher matcher = new AntPathMatcher(); @Override public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException { String requestUrl = ((FilterInvocation) o).getRequestUrl(); if(matcher.match("/",requestUrl) || matcher.match("/toLogin",requestUrl)){ return null;//白名单放行 } List<Menu> allMenus = menuMapper.getAllMenus(); for (Menu menu : allMenus) { if(matcher.match(menu.getPattern(),requestUrl)){ List<Role> roles = menu.getRoles(); String[] roleNames = new String[roles.size()]; for (int i = 0; i < roles.size(); i++) { roleNames[i] = roles.get(i).getName(); } return SecurityConfig.createList(roleNames); } } return SecurityConfig.createList("ROLE_LOGIN");//至少需要登录后才行 } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> aClass) { return true; } }
@Component public class UserAccessDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { ////判断用户角色是否为url所需角色 Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); for (ConfigAttribute configAttribute : configAttributes) { //当前url所需角色 String needRole = configAttribute.getAttribute(); //判断角色是否登录即可访问的角色,此角色在CustomFilter中设置 if ("ROLE_LOGIN".equals(needRole)){ //判断是否登录 if (authentication instanceof AnonymousAuthenticationToken){ throw new AccessDeniedException("尚未登录,请登录!"); }else { return; } } for (GrantedAuthority authority : authorities) { if(needRole.equals(authority.getAuthority())){ return; } } } throw new AccessDeniedException("权限不足"); } @Override public boolean supports(ConfigAttribute attribute) { return false; } @Override public boolean supports(Class<?> clazz) { return false; } }
因为我们做的一般都是web项目,所以实际需要实现的接口是FilterInvocationSecurityMetadataSource
- FilterInvocationSecurityMetadataSource方法使用
1.Collection getAttributes(Object object) throws IllegalArgumentException;
获取某个受保护的安全对象object的所需要的权限信息,是一组ConfigAttribute对象的集合,如果该安全对象object不被当前SecurityMetadataSource对象支持,则抛出异常IllegalArgumentException。
该方法通常配合boolean supports(Class<?> clazz)一起使用,先使用boolean supports(Class<?> clazz)确保安全对象能被当前SecurityMetadataSource支持,然后再调用该方法。(getAttributes方法返回本次访问需要的权限,可以有多个权限。在上面的实现中如果没有匹配的url直接返回null,也就是没有配置权限的url默认都为白名单,想要换成默认是黑名单只要修改这里即可。)
2.Collection getAllConfigAttributes()
获取该SecurityMetadataSource对象中保存的针对所有安全对象的权限信息的集合。该方法的主要目的是被AbstractSecurityInterceptor用于启动时校验每个ConfigAttribute对象。
(getAllConfigAttributes方法如果返回了所有定义的权限资源,Spring Security会在启动时校验每个ConfigAttribute是否配置正确,不需要校验直接返回null。)
3.boolean supports(Class<?> clazz)
这里clazz表示安全对象的类型,该方法用于告知调用者当前SecurityMetadataSource是否支持此类安全对象,只有支持的时候,才能对这类安全对象调用getAttributes方法。(supports方法返回类对象是否支持校验,web项目一般使用FilterInvocation来判断,或者直接返回true。)
- 权限决策 AccessDecisionManager
同样的也有三个方法,其它两个和SecurityMetadataSource类似,这里主要讲讲decide方法。decide方法的三个参数中:
authentication包含了当前的用户信息,包括拥有的权限。这里的权限来源就是前面登录UserDetailsService中设置的authorities。
object就是FilterInvocation对象,可以得到request等web资源。
configAttributes是本次访问需要的权限。
思考1:账号密码错误提示怎么自定义
思考2:前后分离项目怎么使用springsecurity
- 1.配置类主要的区别如下:授权部分
- 2.自定义token过滤器
public class JwtAuthencationTokenFilter extends OncePerRequestFilter { @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader(tokenHeader); //存在token if (null != authHeader && authHeader.startsWith(tokenHead)) { String authToken = authHeader.substring(tokenHead.length()); String username = jwtTokenUtil.getUserNameFromToken(authToken); //token存在用户名但未登录 if (null != username && null == SecurityContextHolder.getContext().getAuthentication()) { //登录 UserDetails userDetails = userDetailsService.loadUserByUsername(username); //验证token是否有效,重新设置用户对象 if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } } filterChain.doFilter(request, response); } }
- 3.当未登录或者token失效自定义的返回结果
@Component public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); PrintWriter out = response.getWriter(); RespBean bean = RespBean.error("尚未登录,请登录!"); bean.setCode(401); out.write(new ObjectMapper().writeValueAsString(bean)); out.flush(); out.close(); } }
- 4.当没有权限时,自定义返回结果
@Component public class RestfulAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); PrintWriter out = response.getWriter(); RespBean bean = RespBean.error("权限不足,请联系管理员!"); bean.setCode(403); out.write(new ObjectMapper().writeValueAsString(bean)); out.flush(); out.close(); } }
好博客就要一起分享哦!分享海报
您可能感兴趣的博客