spring security多种校验方式及自实现token总结

最近项目上运用到了spring security来控制控制权限,然而钉钉端采用的前后端分离的模式进行开发,pc端未采用前后端分离。这样就出现了两个问题:
1.钉钉前端无法携带JSSESSIONID,从而导致spring security框架无法自动解析权限,需要自行实现token并存入redis。
2.钉钉端需要使用手机号码免密登录,pc端需要账号密码进行登录。
功能完成后,此处做个总结。

在SecurityConfig类中注册认证提供者
pc端我采用的是spring security默认的DaoAuthenticationProvider 从数据库中获取账号密码进行比对登录,将userDetailService的实现类改为了自己业务中的实现类。
钉钉端采用的是自行维护相关验证逻辑。

 @Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private HttpStatusEntryPoint httpStatusEntryPoint = new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED);

    /**
     * 成功的处理器
     */
    @Autowired
    private CsoftAuthenticationSuccessHandler csoftAuthenticationSuccessHandler;

    @Autowired
    private CsoftUserDetailService csoftUserDetailService;
    
    @Autowired
    private TokenAuthenticationProvider tokenAuthenticationProvider;
    
    /**
     * 将认证器注册到认证管理器中
     * @param auth
     * @throws Exception
     */
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    //此处将认证提供者注册到管理中心中统一管理
        auth.authenticationProvider(tokenAuthenticationProvider)//自实现token的手机号码登录校验
        .authenticationProvider(daoAuthenticationProvider())//springsecurity 自带的账号密码登录校验
        ;
    }
    /**
     * 失败的处理器
     */
    @Autowired
    private CsoftAuthenticationFailureHandler csoftAuthenticationFailureHandler;

    /**
     * 注入密码加密对象
     * @return Spring原生的加密对象Bean
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    /**
     * 将默认的认证中心中的属性 改为业务中的实现类
     * @return
     */
    @Bean
    DaoAuthenticationProvider daoAuthenticationProvider(){
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        daoAuthenticationProvider.setUserDetailsService(csoftUserDetailService);
        return daoAuthenticationProvider;
    }


    /**
     * 验证码拦截器
     */
    @Autowired
    private ValidateCodeFilter validateCodeFilter;
    
  //注册自定义的UsernamePasswordAuthenticationFilter
    @Bean
    CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
    	
        CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
        filter.setFilterProcessesUrl(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM);
        //这句很关键,重用WebSecurityConfigurerAdapter配置的AuthenticationManager,不然要自己组装AuthenticationManager
        filter.setAuthenticationManager(authenticationManagerBean());
        filter.setAuthenticationSuccessHandler(csoftAuthenticationSuccessHandler);
        filter.setAuthenticationFailureHandler(csoftAuthenticationFailureHandler);
        
        return filter;
    }
    @Override
    @Bean 
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 请求安全策略
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//    	http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        // 跨域攻击防护
        http.cors().and().csrf().disable();
        // 同源打开iframe
        http.headers().frameOptions().sameOrigin();

        /**
         * 通过表单用户名和密码登入验证
         */
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
        	.addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .formLogin().loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)
                .loginProcessingUrl(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM)
                .successHandler(csoftAuthenticationSuccessHandler)
                .failureHandler(csoftAuthenticationFailureHandler)
                .and()
                .authorizeRequests()
                .antMatchers("/account/**").hasAnyAuthority("ROLE_USER")
                .antMatchers("/admin/**").hasAnyAuthority("ROLE_ADMIN")
                .antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL).permitAll()
                .anyRequest().authenticated();
        
        //钉钉端过滤器
        AuthenticationFilter filter = new AuthenticationFilter(authenticationManager(), httpStatusEntryPoint);
        http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
    }

一、手机号码免密登录 通过json格式访问在header中添加Token 访问后台,后台予以解析
第一步:实现AuthenticationProvider

@Component
public class TokenAuthenticationProvider implements AuthenticationProvider {
   private static Logger logger = LoggerFactory.getLogger(TokenAuthenticationProvider.class);

   @Autowired
   private AuthTokenService authTokenService;

   @Override
   public Authentication authenticate(Authentication authentication) throws AuthenticationException {
       String tokenId = authentication.getName();
       
       logger.info(
               "TokenAuthenticationProvider.class: authenticate() - tokenId:" + tokenId );

       UserToken token = null;
       try {
           token = authTokenService.loadTokenById(tokenId);
       } catch (UsernameNotFoundException e) {
           throw new BadCredentialsException("Username not found.");
       }

       if (token == null) {
           throw new BadCredentialsException("Username not found.");
       }

       Collection<? extends GrantedAuthority> authorities = token.getAuthorities();
       return new MobileAuthenticationToken(token, authorities);
   }

   @Override
   public boolean supports(Class<?> authentication) {
   	//将此处改为自己编写的MobileAuthenticationToken类,providerManager会遍历
   	securityconfig中注册的provider,根据此方法返回true或false来决定由哪个provider
   	去校验请求过来的authentication。
       return MobileAuthenticationToken.class
				.isAssignableFrom(authentication);
   }
}

第二步:实现MobileAuthenticationToken

public class MobileAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	// ~ Instance fields
	// ================================================================================================

	private final Object principal;

	// ~ Constructors
	// ===================================================================================================

	/**
	 * This constructor can be safely used by any code that wishes to create a
	 * <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
	 * will return <code>false</code>.
	 *
	 */
	public MobileAuthenticationToken(Object principal) {
		super(null);
		this.principal = principal;
		setAuthenticated(false);
	}

	/**
	 * This constructor should only be used by <code>AuthenticationManager</code> or
	 * <code>AuthenticationProvider</code> implementations that are satisfied with
	 * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
	 * authentication token.
	 *
	 * @param principal
	 * @param credentials
	 * @param authorities
	 */
	public MobileAuthenticationToken(Object principal,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		super.setAuthenticated(true); // must use super, as we override
	}

	// ~ Methods
	// ========================================================================================================

	public Object getPrincipal() {
		return this.principal;
	}

	public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
		if (isAuthenticated) {
			throw new IllegalArgumentException(
					"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
		}

		super.setAuthenticated(false);
	}

	@Override
	public void eraseCredentials() {
		super.eraseCredentials();
	}

	@Override
	public Object getCredentials() {
		return null;
	}

第三步:实现filter用于鉴权

public class AuthenticationFilter extends OncePerRequestFilter {

    ObjectMapper objectMapper;
	
    private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
    private AuthenticationEntryPoint authenticationEntryPoint;
    private AuthenticationManager authenticationManager;
    /**
     * Token 后面 有个空格
     */
    private final static String TOKEN_KEY = "Token ";
    private RememberMeServices rememberMeServices = new NullRememberMeServices();
    private boolean ignoreFailure = false;
    private String credentialsCharset = "UTF-8";
    

    public AuthenticationFilter(AuthenticationManager authenticationManager,
            AuthenticationEntryPoint authenticationEntryPoint) {
        Assert.notNull(authenticationManager, "authenticationManager cannot be null");
        Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
        this.authenticationManager = authenticationManager;
        this.authenticationEntryPoint = authenticationEntryPoint;
        objectMapper = new ObjectMapper();
    }

    @Override
    public void afterPropertiesSet() {
        Assert.notNull(this.authenticationManager, "An AuthenticationManager is required");

        if (!isIgnoreFailure()) {
            Assert.notNull(this.authenticationEntryPoint, "An AuthenticationEntryPoint is required");
        }
    }
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        final boolean debug = logger.isDebugEnabled();

        try {
        	
            String header = request.getHeader("Authorization") == null?"":request.getHeader("Authorization").toString();
            logger.info("header:"+header);

            String uri = request.getRequestURI();

            String token = "";
            if(header.startsWith(TOKEN_KEY)) {
            	token = extractAndDecodeHeader(header, request);
            }
            if (debug) {
                logger.debug("Token Authentication Authorization header found for token '" + token + "'");
            }

            if (authenticationIsRequired(token)) {
                MobileAuthenticationToken authRequest = new MobileAuthenticationToken(token);
                
                
                
                authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
                Authentication authResult = authenticationManager.authenticate(authRequest);

                if (debug) {
                    logger.debug("Authentication success: " + authResult);
                }

                SecurityContextHolder.getContext().setAuthentication(authResult);

                rememberMeServices.loginSuccess(request, response, authResult);

                onSuccessfulAuthentication(request, response, authResult);
            }

        } catch (AuthenticationException failed) {
            if (debug) {
                logger.debug("Authentication request for failed: " + failed);
            }

            rememberMeServices.loginFail(request, response);

            onUnsuccessfulAuthentication(request, response, failed);

            if (ignoreFailure) {
                chain.doFilter(request, response);
            } else {
                authenticationEntryPoint.commence(request, response, failed);
            }
        }

        chain.doFilter(request, response);
    }


    private String extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException {

        byte[] decoded = header.substring(6).getBytes("UTF-8");
        String token = new String(decoded, getCredentialsCharset(request));
        return token;
    }
	//判断是否需要登录
    private boolean authenticationIsRequired(String token) {

        Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
        
        logger.info("existingAuth:"+existingAuth);

        if (existingAuth == null || !existingAuth.isAuthenticated()) {
            return true;
        }

        if (existingAuth instanceof MobileAuthenticationToken && !existingAuth.getName().equals(token)) {
            return true;
        }
        if (existingAuth instanceof AnonymousAuthenticationToken) {
            return true;
        }

        return false;
    }

    protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
            Authentication authResult) throws IOException {
    	
    }

    protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException failed) throws IOException {
	    	logger.info("用户验证失败");
	        String uri = request.getRequestURI();
	        logger.info("uri:"+uri);
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse("验证失败,请重新登录!")));
    }

    protected AuthenticationEntryPoint getAuthenticationEntryPoint() {
        return authenticationEntryPoint;
    }

    protected AuthenticationManager getAuthenticationManager() {
        return authenticationManager;
    }

    protected boolean isIgnoreFailure() {
        return ignoreFailure;
    }

    public void setAuthenticationDetailsSource(
            AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
        Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
        this.authenticationDetailsSource = authenticationDetailsSource;
    }

    public void setRememberMeServices(RememberMeServices rememberMeServices) {
        Assert.notNull(rememberMeServices, "rememberMeServices cannot be null");
        this.rememberMeServices = rememberMeServices;
    }

    public void setCredentialsCharset(String credentialsCharset) {
        Assert.hasText(credentialsCharset, "credentialsCharset cannot be null or empty");
        this.credentialsCharset = credentialsCharset;
    }

    protected String getCredentialsCharset(HttpServletRequest httpRequest) {
        return credentialsCharset;
    }
}

第四步:实现手机号码登陆的controller

@RestController                                                                                                                                                                                                                                                                                                         
public class LoginController {
    private static Logger logger = LoggerFactory.getLogger(LoginController.class);

    @Autowired
    private UserService userService;
    
    @Autowired
    private LoginService loginService;

    @RequestMapping(value = "/login", method = RequestMethod.POST, consumes = { MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_JSON_VALUE })
    @CrossOrigin(origins = "*")
    @ResponseBody
    public LoginResponse loginAction(@RequestBody LoginRequest req, BindingResult result) {

    	LoginResponse res = new LoginResponse();
    	String userName = req.getLoginName();
    	String password = req.getPassword();
    	if(password!=null && !password.equals("")) {
    		//进行加密处理
    		try {
				password = PwdEncoder.encryptBASE64(password.getBytes()).trim();
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
    	}
    	if(req.getMobile()!=null && !"".equals(req.getMobile())) {
    		User user = userService.getUserByMobile(req.getMobile());
    		if(user==null) {
    			res.setResponseCode(AUTH_FAIL);
            	res.setMessage("该手机号尚未开通权限,请联系管理员!");
            	return res;
    		}
    		userName = user.getUsername();
    		password = user.getPassword();
    	}
    	logger.info("password:"+password);
        UserToken token = loginService.authentication(userName, password);
        
        if (token != null) {
            res = new LoginResponse(userName,token.getUserId(), token, token.getAuthorities());
            res.setResponseCode(SUCCESS);
            res.setMessage("Login success for  " + req.getLoginName());
            logger.info("logsuccess:"+userName+" Token:"+token.getId()+" userId:"+token.getUserId());
        } else {
        	res.setResponseCode(AUTH_FAIL);
        	res.setMessage("登入信息有误!");
        	return res;
        }
        return res;
    }

    @RequestMapping(value = "/logout", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
    @CrossOrigin(origins = "*")
    public @ResponseBody LogoffResponse logoffAction() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        UserToken token = (UserToken) authentication.getPrincipal();
        loginService.logoff(token.getId());
        LogoffResponse res = new LogoffResponse(SUCCESS, "logoffSuccess");
        logger.info("loginOff success:"+token.getId());
        return res;
    }

    @ExceptionHandler(BusinessException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public @ResponseBody ErrorResponse handleBusinessError(BusinessException e) {
        ErrorResponse res = new ErrorResponse();
        res.setErrors(e.getErrors());
        res.setResponseCode(AUTH_FAIL);
        return res;
    }
}

二、账号密码登录:

第一步:PC端的表单验证过滤器

public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
	
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)
                || request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
            //use jackson to deserialize json
            ObjectMapper mapper = new ObjectMapper();
            UsernamePasswordAuthenticationToken authRequest = null;
            try (InputStream is = request.getInputStream()) {
                AuthenticationBean authenticationBean = mapper.readValue(is, AuthenticationBean.class);
                if(authenticationBean!=null) {
                	authenticationBean = AuthenticationBean.decode(authenticationBean);    
                authRequest = new UsernamePasswordAuthenticationToken(
                        authenticationBean.getUsername(), authenticationBean.getPassword());
            } catch (IOException e) {
                e.printStackTrace();
                authRequest = new UsernamePasswordAuthenticationToken(
                        "", "");
            } finally {
                setDetails(request, authRequest);
                Authentication authResult = getAuthenticationManager().authenticate(authRequest);
                return authResult;
            }
        }
        else {
            return super.attemptAuthentication(request, response);
        }
    }
}

第二步:自定义的userDetailServiceImpl

@Component
public class CsUserDetailService implements UserDetailsService {

    @Autowired
    private UserDao userDao;
    @Autowired
    private UserRoleDao userRoleDao;
    @Autowired
    private RoleDao roleDao;

    Logger log = LoggerFactory.getLogger(CsUserDetailService.class);

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 如何不正确的用户名直接返回不认证
        if (username == null && username.isEmpty()) {
            return null;
        }
        User user =  userDao.getUserByName(username);      
        log.info("user:"+user);
        if (user==null) {
          throw new UsernameNotFoundException("不存在的用户");
        }


        // 查询出用户权限关联表
       List<UserRole> userRoles = userRoleDao.findUserRolesByUserId(user.getId());
        for (UserRole userRole : userRoles) {
            user.getAuthorities().add(userRole );
        }

        return  user;
    }
  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Security可以通过校验token实现权限的校验。在Spring Security中,可以通过自定义实现UserDetailsService接口的类来加载用户信息,并在loadUserByUsername方法中进行token的验证。具体步骤如下: 1. 创建一个实现UserDetailsService接口的类,在该类中重写loadUserByUsername方法,方法参数为token。 2. 在loadUserByUsername方法中,可以根据token从数据库或其他存储中获取用户信息,并创建一个UserDetails对象。可以使用token来验证用户的身份并获取相应的用户权限。 3. 在loadUserByUsername方法中,可以通过调用UserDetails对象的相关方法来设置用户的权限和其他相关信息。 4. 将创建好的UserDetails对象返回。 通过以上步骤,Spring Security就可以通过校验token实现权限的校验。需要注意的是,具体的token校验逻辑和存储方式可以根据项目需求进行自定义实现。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [SpringSecurity](https://download.csdn.net/download/weixin_42561846/12879985)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [spring security多种校验方式及自实现token总结](https://blog.csdn.net/earthsomeday/article/details/90314067)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值