no15-springsecurity学习

飞一样的编程
飞一样的编程
擅长邻域:Java,MySQL,Linux,nginx,springboot,mongodb,微信小程序,vue

分类: springboot 专栏: springboot学习 标签: springsecurity

2023-04-09 22:36:14 760浏览

springsecurity

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();
	}
}

好博客就要一起分享哦!分享海报

此处可发布评论

评论(1展开评论

蓝色妖姬 能力:10

2023-04-16 14:58:44

回归
点击查看更多评论

展开评论

客服QQ 1913284695