spring security 与springBoot集成

    xiaoxiao2022-07-12  164

    一,导入依赖

    <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>

    二,自定义userDetail 和userDetailService

    userDetail 是security 中对用户信息的封装,security默认使用DaoAuthenticationProvider 去调用userDetailService 获取数据库中的用户信息,然后拿着这个信息去和前端传来的用户信息进行比对,完成认证。

    userDetail

    /** * ① 定义 user 对象 */ public class SelfUserDetails implements UserDetails, Serializable { private String username; private String password; private Set<? extends GrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } public void setAuthorities(Set<? extends GrantedAuthority> authorities) { this.authorities = authorities; } @Override public String getPassword() { // 最重点Ⅰ return this.password; } @Override public String getUsername() { // 最重点Ⅱ return this.username; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }

    userDetailService

    /** * ② 根据 username 获取数据库 user 信息 */ @Component @Slf4j(topic = "security登陆认证") public class SelfUserDetailsService implements UserDetailsService { @Autowired private TUsersDao userDao; @Autowired private AgentInfoDao agentInfoDao; /** * @param username 用户名或手机号 * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //构建用户信息的逻辑(取数据库/LDAP等用户信息) SelfUserDetails userInfo = new SelfUserDetails(); TUsers users = userDao.findByUsername(username); if (users == null) users = userDao.findByMobile(username); if (users == null) throw new UsernameNotFoundException("用户不存在"); userInfo.setUsername(users.getUsername()); // 任意用户名登录 userInfo.setPassword(users.getPassword()); //后台管理中数据库中角色分为, 普通用户(还没有进件的代理商),代理商 Set<GrantedAuthority> authoritiesSet = new HashSet<>(); GrantedAuthority authority = null; TAgentInfo agentInfo = agentInfoDao.findByUserId(users.getId()); if ( agentInfo== null){ authority = new SimpleGrantedAuthority(Constant.CmsRole.USER); }else { //存在代理商信息,但是代理商没有通过审核,仍然为普通用户权限 if (agentInfo.getCheckStatus() != 1){ authority = new SimpleGrantedAuthority(Constant.CmsRole.USER); }else { //代理商通过审核, authority = new SimpleGrantedAuthority(Constant.CmsRole.AGENT); } } authoritiesSet.add(authority); userInfo.setAuthorities(authoritiesSet); log.info("数据库用户名:'{}',用户角色:'{}'",users.getUsername(),authority.getAuthority()); return userInfo; } }

    三, 自定义handler

    security 对于登陆失败和登陆成功,权限不足都有各自的jandler,前后端分离模式下,需要重新定义。

    AccessDeniedHandler (登陆之后发生的权限不足)

    /** * 登陆之后发生的权限不足的处理器 */ @Component public class AjaxAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { ResponseBean responseBody = new ResponseBean(); //TODO 权限不足 responseBody.setCode(300); responseBody.setMsg("权限不足,联系管理员!"); httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json;charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(responseBody)); } }

    AuthenticationEntryPoint(未登录权限不足)

    /** * 未登陆不能访问url的处理器 */ @Component public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { ResponseBean responseBody = new ResponseBean(); responseBody.setCode(400); responseBody.setMsg("没有权限,请先登陆"); httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json;charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(responseBody)); } }

    AuthenticationFailureHandler(登陆失败处理器)

    @Component public class AjaxAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { ResponseBean responseBody = new ResponseBean(); responseBody.setCode(400); responseBody.setMsg("登陆失败!"); httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json;charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(responseBody)); } }

    AuthenticationSuccessHandler(登陆成功处理器)

    /** * 登陆成功处理器 */ @Component public class AjaxAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { ResponseBean responseBody = new ResponseBean(); responseBody.setCode(200); responseBody.setMsg("登陆成功!"); httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json;charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(responseBody)); } }

    LogoutSuccessHandler(退出成功处理器)

    @Component public class AjaxLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { ResponseBean responseBody = new ResponseBean(); responseBody.setCode(200); responseBody.setMsg("退出成功!"); httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json;charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(responseBody)); } }

    ## 四,配置类

    @Configuration //开启方法注解 @EnableGlobalMethodSecurity(prePostEnabled =true) public class SpringSecurityConf extends WebSecurityConfigurerAdapter { @Autowired AjaxAuthenticationEntryPoint authenticationEntryPoint; // 未登陆时返回 JSON 格式的数据给前端(否则为 html) @Autowired AjaxAuthenticationSuccessHandler authenticationSuccessHandler; // 登录成功返回的 JSON 格式数据给前端(否则为 html) @Autowired AjaxAuthenticationFailureHandler authenticationFailureHandler; // 登录失败返回的 JSON 格式数据给前端(否则为 html) @Autowired AjaxLogoutSuccessHandler logoutSuccessHandler; // 注销成功返回的 JSON 格式数据给前端(否则为 登录时的 html) @Autowired AjaxAccessDeniedHandler accessDeniedHandler; // 无权访问返回的 JSON 格式数据给前端(否则为 403 html 页面) @Autowired private SelfUserDetailsService userDetailsService; @Autowired private SmsProvider smsProvider; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //新增登陆入口 auth.authenticationProvider(smsProvider); auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder()); //自定义用户 auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("xch") .password(new BCryptPasswordEncoder().encode("123")).roles(Constant.CmsRole.ADMIN); } @Bean GrantedAuthorityDefaults grantedAuthorityDefaults() { // Remove the ROLE_ prefix return new GrantedAuthorityDefaults(""); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .formLogin() //表单登陆的url地址,默认参数名username,password .loginProcessingUrl("/login") .successHandler(authenticationSuccessHandler) // 登录成功 .failureHandler(authenticationFailureHandler) // 登录失败 .permitAll() .and() .authorizeRequests() .antMatchers("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**","/error","/doc.html").permitAll() .antMatchers( Constant.RedisKey.PC_REG_SMS, Constant.RedisKey.PC_LOG_SMS ,"/sms/**").permitAll() .anyRequest().authenticated()// 其他 url 需要身份认证 .and() .logout() .logoutSuccessHandler(logoutSuccessHandler) .permitAll(); http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); // 无权访问 JSON 格式的数据 SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter(Constant.RedisKey.PC_LOG_SMS); smsAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler); smsAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler); smsAuthenticationFilter.setAuthenticationManager(authenticationManagerBean()); http.addFilterBefore(smsAuthenticationFilter,UsernamePasswordAuthenticationFilter.class); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }

    五, 新增短信登陆入口

    security 认证思路:

    manager 管理多个provider,一个provider通过surpports可以验证多个或一个token,一般来说,一个应用只有一个manager,authenticationFilter 构建未验证的token,交由manager,manager去寻找特定的provider去验证token。验证成功后的token将放入SecurityContextHolder(Threadlocal的封装)中,然后请求结束时,security有个过滤器,会将验证之后的token放入session中,下次请求便不用重新登陆。

    AuthenticationFilter AuthenticationManager AuthenticationProvider 返回认证之后的AuthenticationToken

    自定义一套短信登陆的filter,provider

    之前security配置开启表单登陆,默认使用security自带的DaoAuthenticationProvider ,UsernamePasswordAuthenticationFilter。新增短信登陆需要自己实现一套这样的机制。

    /** * 短信验证登陆 * * @author xch * @since 2019/5/15 16:45 **/ public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public SmsAuthenticationFilter(String defaultFilterProcessesUrl) { super(defaultFilterProcessesUrl); } @Override public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException { String uri = httpServletRequest.getRequestURI(); String mobile = null; String code = null; if (uri.equals(Constant.RedisKey.PC_LOG_SMS) && httpServletRequest.getMethod().equals("POST")) { mobile = httpServletRequest.getParameter("mobile"); code = httpServletRequest.getParameter("code"); if (mobile == null) { mobile = ""; } mobile = mobile.trim(); SmsToken smsToken = new SmsToken(mobile, code); smsToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest)); //交由AuthenticationManager 去认证token return this.getAuthenticationManager().authenticate(smsToken); } else { throw new AuthenticationServiceException("Authentication method not supported: " + httpServletRequest.getMethod()); } } }

    provider

    /** * @author xch * @since 2019/5/15 16:56 **/ @Component public class SmsProvider implements AuthenticationProvider { @Autowired private SelfUserDetailsService userDetailsService; @Autowired private StringRedisTemplate redisTemplate; /** * 获取手机号-》获取验证码-》与缓存中验证码进行比对-》成功,放入token,失败,返回前端数据 * @param authentication * @return * @throws AuthenticationException */ @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String mobile = (String) authentication.getPrincipal(); String code = (String) authentication.getCredentials(); String codeInRedis = redisTemplate.opsForValue().get(String.join(":", Constant.RedisKey.PC_LOG_SMS, mobile)); if (StringUtils.equals(codeInRedis, code)) { UserDetails userDetails = userDetailsService.loadUserByUsername(mobile); return new SmsToken(userDetails.getAuthorities(),userDetails.getUsername(),userDetails.getPassword()); } throw new BadCredentialsException("验证码错误"); } //一个provider 支持验证某种特定类型的token @Override public boolean supports(Class<?> authentication) { return SmsToken.class.isAssignableFrom(authentication); } }

    token

    /** * sms token * @author xch * @since 2019/5/15 17:13 **/ public class SmsToken extends AbstractAuthenticationToken { private String username; private String credentials; public SmsToken(String mobile,String credentials) { super(null); this.username = mobile; this.setAuthenticated(false); this.credentials = credentials; } public SmsToken(Collection<? extends GrantedAuthority> authorities,String username,String credentials) { super(authorities); this.username = username; this.credentials = credentials; this.setAuthenticated(true); } @Override public String getCredentials() { return credentials; } @Override public String getPrincipal() { return username; } }

    配置类中为filter配置处理器

    SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter(Constant.RedisKey.PC_LOG_SMS); smsAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler); smsAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler); smsAuthenticationFilter.setAuthenticationManager(authenticationManagerBean()); http.addFilterBefore(smsAuthenticationFilter,UsernamePasswordAuthenticationFilter.class);
    最新回复(0)